Module:FamilyTree: Difference between revisions
From KB Lexicon
No edit summary |
No edit summary |
||
| Line 3: | Line 3: | ||
local cargo = mw.ext.cargo | local cargo = mw.ext.cargo | ||
local html = mw.html | local html = mw.html | ||
local function trim(s) | local function trim(s) | ||
if s == nil then | if s == nil then return nil end | ||
s = tostring(s) | s = tostring(s) | ||
s = mw.text.trim(s) | s = mw.text.trim(s) | ||
if s == '' then | if s == '' then return nil end | ||
return s | return s | ||
end | end | ||
| Line 22: | Line 14: | ||
local function isRealValue(v) | local function isRealValue(v) | ||
v = trim(v) | v = trim(v) | ||
if not v then | if not v then return false end | ||
local lowered = mw.ustring.lower(v) | local lowered = mw.ustring.lower(v) | ||
return lowered ~= 'unknown' and lowered ~= 'none' and lowered ~= 'n/a' | return lowered ~= 'unknown' and lowered ~= 'none' and lowered ~= 'n/a' | ||
| Line 30: | Line 20: | ||
local function addUnique(list, value) | local function addUnique(list, value) | ||
if not isRealValue(value) then | if not isRealValue(value) then return end | ||
for _, existing in ipairs(list) do | for _, existing in ipairs(list) do | ||
if existing == value then | if existing == value then return end | ||
end | end | ||
table.insert(list, value) | table.insert(list, value) | ||
| Line 42: | Line 28: | ||
local function uniq(list) | local function uniq(list) | ||
local out = {} | local out, seen = {}, {} | ||
for _, v in ipairs(list or {}) do | for _, v in ipairs(list or {}) do | ||
if isRealValue(v) and not seen[v] then | if isRealValue(v) and not seen[v] then | ||
| Line 55: | Line 40: | ||
local function getArg(frame, key) | local function getArg(frame, key) | ||
local v = frame.args[key] | local v = frame.args[key] | ||
if isRealValue(v) then | if isRealValue(v) then return trim(v) end | ||
local parent = frame:getParent() | local parent = frame:getParent() | ||
if parent then | if parent then | ||
v = parent.args[key] | v = parent.args[key] | ||
if isRealValue(v) then | if isRealValue(v) then return trim(v) end | ||
end | end | ||
return nil | return nil | ||
end | end | ||
| Line 75: | Line 54: | ||
local function makeLink(name, displayName) | local function makeLink(name, displayName) | ||
if not isRealValue(name) then | if not isRealValue(name) then return '' end | ||
displayName = trim(displayName) or name | displayName = trim(displayName) or name | ||
return string.format('[[%s|%s]]', name, displayName) | return string.format('[[%s|%s]]', name, displayName) | ||
| Line 84: | Line 61: | ||
local function ensurePerson(people, name) | local function ensurePerson(people, name) | ||
name = trim(name) | name = trim(name) | ||
if not isRealValue(name) then | if not isRealValue(name) then return nil end | ||
if not people[name] then | if not people[name] then | ||
| Line 99: | Line 74: | ||
} | } | ||
end | end | ||
return people[name] | return people[name] | ||
end | end | ||
| Line 111: | Line 85: | ||
end | end | ||
-- Data loading | -- Data loading | ||
local function loadCharacters() | local function loadCharacters() | ||
local results = cargo.query( | local results = cargo.query('Characters', 'Page,DisplayName', { limit = 5000 }) | ||
local people = {} | local people = {} | ||
| Line 127: | Line 94: | ||
local page = trim(row.Page) | local page = trim(row.Page) | ||
local displayName = trim(row.DisplayName) | local displayName = trim(row.DisplayName) | ||
if isRealValue(page) then | if isRealValue(page) then | ||
people[page] = { | people[page] = { | ||
| Line 140: | Line 106: | ||
end | end | ||
end | end | ||
return people | return people | ||
end | end | ||
| Line 166: | Line 131: | ||
addUnique(people[child].parents, p1) | addUnique(people[child].parents, p1) | ||
addUnique(people[p1].children, child) | addUnique(people[p1].children, child) | ||
table.insert(people[p1].childLinks, { | table.insert(people[p1].childLinks, { | ||
child = child, | child = child, | ||
| Line 180: | Line 144: | ||
addUnique(people[child].parents, p2) | addUnique(people[child].parents, p2) | ||
addUnique(people[p2].children, child) | addUnique(people[p2].children, child) | ||
table.insert(people[p2].childLinks, { | table.insert(people[p2].childLinks, { | ||
child = child, | child = child, | ||
| Line 204: | Line 167: | ||
local p2 = trim(row.Partner2) | local p2 = trim(row.Partner2) | ||
if isRealValue(p1) then | if isRealValue(p1) then ensurePerson(people, p1) end | ||
if isRealValue(p2) then ensurePerson(people, p2) end | |||
if isRealValue(p2) then | |||
if isRealValue(p1) and isRealValue(p2) then | if isRealValue(p1) and isRealValue(p2) then | ||
| Line 258: | Line 217: | ||
end | end | ||
-- Relationship helpers | -- Relationship helpers | ||
local function relationshipBadge(relType) | local function relationshipBadge(relType) | ||
if not isRealValue(relType) then | if not isRealValue(relType) then return nil end | ||
local t = mw.ustring.lower(relType) | local t = mw.ustring.lower(relType) | ||
if t:find('adopt') then return 'adopted' end | |||
if t:find('adopt') then | if t:find('step') then return 'step' end | ||
if t:find('bio') then return nil end | |||
if t:find('step') then | |||
if t:find('bio') then | |||
return relType | return relType | ||
end | end | ||
local function findUnionBetween(people, name1, name2) | local function findUnionBetween(people, name1, name2) | ||
if not isRealValue(name1) or not isRealValue(name2) then | if not isRealValue(name1) or not isRealValue(name2) then return nil end | ||
local person = people[name1] | local person = people[name1] | ||
if not person or not person.unions then | if not person or not person.unions then return nil end | ||
for _, union in ipairs(person.unions) do | for _, union in ipairs(person.unions) do | ||
if union.partner == name2 then | if union.partner == name2 then return union end | ||
end | end | ||
return nil | return nil | ||
end | end | ||
local function getMarriageYear(union) | local function getMarriageYear(union) | ||
if not union then | if not union then return nil end | ||
local raw = union.marriageDate or union.engagementDate or union.startDate | local raw = union.marriageDate or union.engagementDate or union.startDate | ||
if not isRealValue(raw) then | if not isRealValue(raw) then return nil end | ||
return tostring(raw):match('^(%d%d%d%d)') or tostring(raw) | |||
end | end | ||
local function getParents(people, root) | local function getParents(people, root) | ||
local person = people[root] | local person = people[root] | ||
if not person then | if not person then return {} end | ||
local parents = uniq(person.parents) | local parents = uniq(person.parents) | ||
sortNames(people, parents) | sortNames(people, parents) | ||
| Line 328: | Line 256: | ||
local out = {} | local out = {} | ||
local parents = getParents(people, root) | local parents = getParents(people, root) | ||
for _, parentName in ipairs(parents) do | for _, parentName in ipairs(parents) do | ||
local parent = people[parentName] | local parent = people[parentName] | ||
| Line 337: | Line 264: | ||
end | end | ||
end | end | ||
out = uniq(out) | out = uniq(out) | ||
sortNames(people, out) | sortNames(people, out) | ||
| Line 344: | Line 270: | ||
local function getSiblings(people, root) | local function getSiblings(people, root) | ||
local out = {} | local out, seen = {}, {} | ||
local person = people[root] | local person = people[root] | ||
if not person then return out end | |||
if not person then | |||
for _, parentName in ipairs(person.parents) do | for _, parentName in ipairs(person.parents) do | ||
| Line 372: | Line 294: | ||
local out = {} | local out = {} | ||
local person = people[root] | local person = people[root] | ||
if not person then | if not person then return out end | ||
for _, v in ipairs(person.parents) do addUnique(out, v) 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.children) do addUnique(out, v) end | ||
for _, v in ipairs(person.partners) do addUnique(out, v) end | for _, v in ipairs(person.partners) do addUnique(out, v) end | ||
for _, v in ipairs(getSiblings(people, root)) do addUnique(out, v) end | |||
for _, v in ipairs(getGrandparents(people, root)) do addUnique(out, v) end | |||
for _, v in ipairs( | |||
for _, v in ipairs( | |||
out = uniq(out) | out = uniq(out) | ||
| Line 393: | Line 309: | ||
local function getRootSiblingSequence(people, root) | local function getRootSiblingSequence(people, root) | ||
local siblings = getSiblings(people, root) | local siblings = getSiblings(people, root) | ||
local seq = {} | local seq, inserted = {}, false | ||
local midpoint = math.floor(#siblings / 2) + 1 | local midpoint = math.floor(#siblings / 2) + 1 | ||
| Line 414: | Line 329: | ||
local function getFamilyGroupsForRoot(people, root) | local function getFamilyGroupsForRoot(people, root) | ||
local person = people[root] | local person = people[root] | ||
if not person or not person.childLinks then | if not person or not person.childLinks then return {} end | ||
local groups = {} | local groups = {} | ||
| Line 447: | Line 360: | ||
local out = {} | local out = {} | ||
for _, group in pairs(groups) do | for _, group in pairs(groups) do | ||
table.sort(group.children, function(a, b) | table.sort(group.children, function(a, b) return a.birthOrder < b.birthOrder end) | ||
table.insert(out, group) | table.insert(out, group) | ||
end | end | ||
| Line 456: | Line 367: | ||
local aSingle = not isRealValue(a.partner) | local aSingle = not isRealValue(a.partner) | ||
local bSingle = not isRealValue(b.partner) | local bSingle = not isRealValue(b.partner) | ||
if aSingle ~= bSingle then return aSingle end | |||
local ap, bp = a.partner or '', b.partner or '' | |||
local ap = a.partner or '' | |||
local ad = (people[ap] and people[ap].displayName) or ap | local ad = (people[ap] and people[ap].displayName) or ap | ||
local bd = (people[bp] and people[bp].displayName) or bp | local bd = (people[bp] and people[bp].displayName) or bp | ||
| Line 471: | Line 378: | ||
end | end | ||
-- Rendering | |||
-- Rendering | |||
local function renderCard(people, name, badgeText, extraClass) | local function renderCard(people, name, badgeText, extraClass) | ||
if not isRealValue(name) then | if not isRealValue(name) then return nil end | ||
local person = people[name] or { name = name, displayName = name } | local person = people[name] or { name = name, displayName = name } | ||
local card = html.create('div') | local card = html.create('div') | ||
card:addClass(' | card:addClass('kbft-card') | ||
if isRealValue(extraClass) then | if isRealValue(extraClass) then | ||
card:addClass(extraClass) | card:addClass(extraClass) | ||
end | end | ||
card:wikitext(makeLink(person.name, person.displayName)) | card:wikitext(makeLink(person.name, person.displayName)) | ||
if isRealValue(badgeText) then | if isRealValue(badgeText) then | ||
card:tag('div') | card:tag('div') | ||
:addClass(' | :addClass('kbft-years') | ||
:wikitext(badgeText) | :wikitext(badgeText) | ||
end | end | ||
| Line 501: | Line 402: | ||
local function renderSingleCard(people, name, extraClass) | local function renderSingleCard(people, name, extraClass) | ||
local wrap = html.create('div') | local wrap = html.create('div') | ||
wrap:addClass(' | wrap:addClass('kbft-single') | ||
wrap:node(renderCard(people, name, nil, extraClass)) | wrap:node(renderCard(people, name, nil, extraClass)) | ||
return wrap | return wrap | ||
| Line 509: | Line 410: | ||
if isRealValue(leftName) and isRealValue(rightName) then | if isRealValue(leftName) and isRealValue(rightName) then | ||
local wrap = html.create('div') | local wrap = html.create('div') | ||
wrap:addClass(' | wrap:addClass('kbft-couple') | ||
wrap:node(renderCard(people, leftName, nil, leftClass)) | wrap:node(renderCard(people, leftName, nil, leftClass)) | ||
local marriage = wrap:tag('div') | local marriage = wrap:tag('div') | ||
marriage:addClass(' | marriage:addClass('kbft-marriage') | ||
if isRealValue(marriageYear) then | if isRealValue(marriageYear) then | ||
marriage:tag('div') | marriage:tag('div') | ||
:addClass(' | :addClass('kbft-marriage-year') | ||
:wikitext(marriageYear) | :wikitext(marriageYear) | ||
end | end | ||
marriage:tag('div') | marriage:tag('div') | ||
:addClass(' | :addClass('kbft-marriage-line') | ||
wrap:node(renderCard(people, rightName, nil, rightClass)) | wrap:node(renderCard(people, rightName, nil, rightClass)) | ||
| Line 529: | Line 430: | ||
end | end | ||
if isRealValue(leftName) then | if isRealValue(leftName) then return renderSingleCard(people, leftName, leftClass) end | ||
if isRealValue(rightName) then return renderSingleCard(people, rightName, rightClass) end | |||
if isRealValue(rightName) then | |||
return nil | return nil | ||
end | end | ||
| Line 542: | Line 437: | ||
local function renderGenerationRow(units) | local function renderGenerationRow(units) | ||
local row = html.create('div') | local row = html.create('div') | ||
row:addClass(' | row:addClass('kbft-row') | ||
for _, unit in ipairs(units) do | for _, unit in ipairs(units) do | ||
if unit then | if unit then row:node(unit) end | ||
end | end | ||
return row | return row | ||
end | end | ||
| Line 555: | Line 446: | ||
local function renderChildCards(people, children) | local function renderChildCards(people, children) | ||
local childrenWrap = html.create('div') | local childrenWrap = html.create('div') | ||
childrenWrap:addClass(' | childrenWrap:addClass('kbft-children') | ||
for _, child in ipairs(children) do | for _, child in ipairs(children) do | ||
childrenWrap:node( | childrenWrap:node(renderCard(people, child.name, relationshipBadge(child.relationshipType))) | ||
end | end | ||
return childrenWrap | return childrenWrap | ||
end | end | ||
| Line 572: | Line 455: | ||
local function renderFamilyUnit(people, root, group) | local function renderFamilyUnit(people, root, group) | ||
local unit = html.create('div') | local unit = html.create('div') | ||
unit:addClass(' | unit:addClass('kbft-sibling-unit') | ||
local top = unit:tag('div') | local top = unit:tag('div') | ||
top:addClass(' | top:addClass('kbft-family-main-wrap') | ||
if isRealValue(group.partner) then | if isRealValue(group.partner) then | ||
local union = findUnionBetween(people, root, group.partner) | local union = findUnionBetween(people, root, group.partner) | ||
local marriageYear = getMarriageYear(union) | local marriageYear = getMarriageYear(union) | ||
top:node(renderCouple(people, root, group.partner, marriageYear | top:node(renderCouple(people, root, group.partner, marriageYear)) | ||
else | else | ||
top:node(renderSingleCard(people, root)) | |||
end | end | ||
if #group.children > 0 then | if #group.children > 0 then | ||
unit:tag('div') | unit:tag('div'):addClass('kbft-child-down') | ||
unit:node(renderChildCards(people, group.children)) | unit:node(renderChildCards(people, group.children)) | ||
end | end | ||
| Line 598: | Line 477: | ||
local function renderUpperCoupleGeneration(people, couples) | local function renderUpperCoupleGeneration(people, couples) | ||
if #couples == 0 then | if #couples == 0 then return nil end | ||
local gen = html.create('div') | local gen = html.create('div') | ||
gen:addClass(' | gen:addClass('kbft-generation') | ||
local units = {} | local units = {} | ||
| Line 634: | Line 510: | ||
local function buildParentCouples(people, root) | local function buildParentCouples(people, root) | ||
local parents = getParents(people, root) | local parents = getParents(people, root) | ||
if #parents == 0 then | if #parents == 0 then return {} end | ||
return { { parents[1], parents[2] } } | return { { parents[1], parents[2] } } | ||
end | end | ||
| Line 644: | Line 518: | ||
local gen = html.create('div') | local gen = html.create('div') | ||
gen:addClass(' | gen:addClass('kbft-generation') | ||
local groupWrap = gen:tag('div') | |||
groupWrap:addClass('kbft-siblings') | |||
groupWrap:tag('div') | |||
:addClass('kbft-sibling-spine') | |||
local row = groupWrap:tag('div') | |||
row:addClass('kbft-sibling-row') | |||
for _, name in ipairs(sequence) do | for _, name in ipairs(sequence) do | ||
local unit = row:tag('div') | |||
unit:addClass('kbft-sibling-unit') | |||
unit:tag('div') | |||
:addClass('kbft-sibling-up') | |||
if name == root then | if name == root then | ||
unit:node(renderSingleCard(people, name, 'kbft-focus-card')) | |||
else | else | ||
unit:node(renderSingleCard(people, name)) | |||
end | end | ||
end | end | ||
return gen | return gen | ||
| Line 668: | Line 548: | ||
local function renderFamilyGroupsGeneration(people, root) | local function renderFamilyGroupsGeneration(people, root) | ||
local groups = getFamilyGroupsForRoot(people, root) | local groups = getFamilyGroupsForRoot(people, root) | ||
if #groups == 0 then | if #groups == 0 then return nil end | ||
local gen = html.create('div') | local gen = html.create('div') | ||
gen:addClass(' | gen:addClass('kbft-generation') | ||
local units = {} | local units = {} | ||
| Line 684: | Line 562: | ||
end | end | ||
-- Public renderers | -- Public renderers | ||
local function renderConnectedForRoot(people, root) | local function renderConnectedForRoot(people, root) | ||
| Line 695: | Line 571: | ||
local connected = getConnectedPeople(people, root) | local connected = getConnectedPeople(people, root) | ||
local node = html.create('div') | local node = html.create('div') | ||
node:addClass(' | node:addClass('kbft-tree') | ||
node:tag('div') | node:tag('div') | ||
:addClass(' | :addClass('kbft-title') | ||
:wikitext('Connected to ' .. makeLink(person.name, person.displayName)) | :wikitext('Connected to ' .. makeLink(person.name, person.displayName)) | ||
local gen = node:tag('div') | |||
gen:addClass('kbft-generation') | |||
local units = {} | local units = {} | ||
| Line 707: | Line 585: | ||
table.insert(units, renderSingleCard(people, name)) | table.insert(units, renderSingleCard(people, name)) | ||
end | end | ||
gen:node(renderGenerationRow(units)) | gen:node(renderGenerationRow(units)) | ||
| Line 722: | Line 597: | ||
local node = html.create('div') | local node = html.create('div') | ||
node:addClass(' | node:addClass('kbft-tree') | ||
node:tag('div') | node:tag('div') | ||
:addClass(' | :addClass('kbft-title') | ||
:wikitext(makeLink(person.name, person.displayName)) | :wikitext(makeLink(person.name, person.displayName)) | ||
local function addSection(label, names) | local function addSection(label, names) | ||
names = uniq(names) | names = uniq(names) | ||
if #names == 0 then | if #names == 0 then return end | ||
sortNames(people, names) | sortNames(people, names) | ||
node:tag('div') | node:tag('div') | ||
:addClass(' | :addClass('kbft-title') | ||
:css('margin-top', '22px') | |||
:wikitext(label) | :wikitext(label) | ||
local gen = node:tag('div') | |||
gen:addClass('kbft-generation') | |||
local units = {} | local units = {} | ||
| Line 744: | Line 620: | ||
table.insert(units, renderSingleCard(people, name)) | table.insert(units, renderSingleCard(people, name)) | ||
end | end | ||
gen:node(renderGenerationRow(units)) | gen:node(renderGenerationRow(units)) | ||
end | end | ||
| Line 764: | Line 637: | ||
local node = html.create('div') | local node = html.create('div') | ||
node:addClass(' | node:addClass('kbft-tree') | ||
node:tag('div') | node:tag('div') | ||
:addClass(' | :addClass('kbft-title') | ||
:wikitext('Family Tree: ' .. makeLink(person.name, person.displayName)) | :wikitext('Family Tree: ' .. makeLink(person.name, person.displayName)) | ||
| Line 773: | Line 646: | ||
if gpGen then | if gpGen then | ||
node:node(gpGen) | node:node(gpGen) | ||
node:tag('div'):addClass(' | node:tag('div'):addClass('kbft-connector') | ||
end | end | ||
| Line 779: | Line 652: | ||
if parentGen then | if parentGen then | ||
node:node(parentGen) | node:node(parentGen) | ||
node:tag('div'):addClass(' | node:tag('div'):addClass('kbft-connector') | ||
end | end | ||
| Line 786: | Line 659: | ||
local familyGen = renderFamilyGroupsGeneration(people, root) | local familyGen = renderFamilyGroupsGeneration(people, root) | ||
if familyGen then | if familyGen then | ||
node:tag('div'):addClass(' | node:tag('div'):addClass('kbft-connector') | ||
node:node(familyGen) | node:node(familyGen) | ||
end | end | ||
| Line 793: | Line 666: | ||
end | end | ||
-- Public functions | -- Public functions | ||
function p.tree(frame) | function p.tree(frame) | ||
Revision as of 12:08, 30 March 2026
Documentation for this module may be created at Module:FamilyTree/doc
local p = {}
local cargo = mw.ext.cargo
local html = mw.html
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, 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 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 getRoot(frame)
return getArg(frame, 'root') or getArg(frame, 'person') or mw.title.getCurrentTitle().text
end
local function makeLink(name, displayName)
if not isRealValue(name) then return '' end
displayName = trim(displayName) or name
return string.format('[[%s|%s]]', name, displayName)
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,
parents = {},
children = {},
partners = {},
unions = {},
childLinks = {}
}
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
-- Data loading
local function loadCharacters()
local results = cargo.query('Characters', 'Page,DisplayName', { limit = 5000 })
local people = {}
for _, row in ipairs(results) do
local page = trim(row.Page)
local displayName = trim(row.DisplayName)
if isRealValue(page) then
people[page] = {
name = page,
displayName = displayName or page,
parents = {},
children = {},
partners = {},
unions = {},
childLinks = {}
}
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)
local unionID = trim(row.UnionID)
local relationshipType = trim(row.RelationshipType)
local birthOrder = tonumber(trim(row.BirthOrder)) or 999
if isRealValue(child) then
ensurePerson(people, child)
if isRealValue(p1) then
ensurePerson(people, p1)
addUnique(people[child].parents, p1)
addUnique(people[p1].children, child)
table.insert(people[p1].childLinks, {
child = child,
otherParent = p2,
unionID = unionID,
relationshipType = relationshipType,
birthOrder = birthOrder
})
end
if isRealValue(p2) then
ensurePerson(people, p2)
addUnique(people[child].parents, p2)
addUnique(people[p2].children, child)
table.insert(people[p2].childLinks, {
child = child,
otherParent = p1,
unionID = unionID,
relationshipType = relationshipType,
birthOrder = birthOrder
})
end
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 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 = trim(row.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 = trim(row.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 = loadCharacters()
loadParentChild(people)
loadUnions(people)
finalizePeople(people)
return people
end
-- Relationship helpers
local function relationshipBadge(relType)
if not isRealValue(relType) then return nil end
local t = mw.ustring.lower(relType)
if t:find('adopt') then return 'adopted' end
if t:find('step') then return 'step' end
if t:find('bio') then return nil end
return relType
end
local function findUnionBetween(people, name1, name2)
if not isRealValue(name1) or not isRealValue(name2) then return nil end
local person = people[name1]
if not person or not person.unions then return nil end
for _, union in ipairs(person.unions) do
if union.partner == name2 then return union end
end
return nil
end
local function getMarriageYear(union)
if not union then return nil end
local raw = union.marriageDate or union.engagementDate or union.startDate
if not isRealValue(raw) then return nil end
return tostring(raw):match('^(%d%d%d%d)') or tostring(raw)
end
local function getParents(people, root)
local person = people[root]
if not person then return {} end
local parents = uniq(person.parents)
sortNames(people, parents)
return parents
end
local function getGrandparents(people, root)
local out = {}
local parents = getParents(people, root)
for _, parentName in ipairs(parents) do
local parent = people[parentName]
if parent then
for _, gp in ipairs(parent.parents) do
addUnique(out, gp)
end
end
end
out = uniq(out)
sortNames(people, out)
return out
end
local function getSiblings(people, root)
local out, seen = {}, {}
local person = people[root]
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 ~= root and not seen[childName] then
seen[childName] = true
table.insert(out, childName)
end
end
end
end
out = uniq(out)
sortNames(people, out)
return out
end
local function getConnectedPeople(people, root)
local out = {}
local person = people[root]
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
for _, v in ipairs(getSiblings(people, root)) do addUnique(out, v) end
for _, v in ipairs(getGrandparents(people, root)) do addUnique(out, v) end
out = uniq(out)
sortNames(people, out)
return out
end
local function getRootSiblingSequence(people, root)
local siblings = getSiblings(people, root)
local seq, inserted = {}, false
local midpoint = math.floor(#siblings / 2) + 1
for i, sib in ipairs(siblings) do
if i == midpoint then
table.insert(seq, root)
inserted = true
end
table.insert(seq, sib)
end
if not inserted then
table.insert(seq, root)
end
return seq
end
local function getFamilyGroupsForRoot(people, root)
local person = people[root]
if not person or not person.childLinks then return {} end
local groups = {}
for _, link in ipairs(person.childLinks) do
local key
if isRealValue(link.unionID) then
key = 'union::' .. link.unionID
elseif isRealValue(link.otherParent) then
key = 'partner::' .. link.otherParent
else
key = 'single::' .. root
end
if not groups[key] then
groups[key] = {
unionID = link.unionID,
partner = link.otherParent,
children = {}
}
end
table.insert(groups[key].children, {
name = link.child,
relationshipType = link.relationshipType,
birthOrder = tonumber(link.birthOrder) or 999
})
end
local out = {}
for _, group in pairs(groups) do
table.sort(group.children, function(a, b) return a.birthOrder < b.birthOrder end)
table.insert(out, group)
end
table.sort(out, function(a, b)
local aSingle = not isRealValue(a.partner)
local bSingle = not isRealValue(b.partner)
if aSingle ~= bSingle then return aSingle end
local ap, bp = a.partner or '', b.partner or ''
local ad = (people[ap] and people[ap].displayName) or ap
local bd = (people[bp] and people[bp].displayName) or bp
return mw.ustring.lower(ad) < mw.ustring.lower(bd)
end)
return out
end
-- Rendering
local function renderCard(people, name, badgeText, extraClass)
if not isRealValue(name) then return nil end
local person = people[name] or { name = name, displayName = name }
local card = html.create('div')
card:addClass('kbft-card')
if isRealValue(extraClass) then
card:addClass(extraClass)
end
card:wikitext(makeLink(person.name, person.displayName))
if isRealValue(badgeText) then
card:tag('div')
:addClass('kbft-years')
:wikitext(badgeText)
end
return card
end
local function renderSingleCard(people, name, extraClass)
local wrap = html.create('div')
wrap:addClass('kbft-single')
wrap:node(renderCard(people, name, nil, extraClass))
return wrap
end
local function renderCouple(people, leftName, rightName, marriageYear, leftClass, rightClass)
if isRealValue(leftName) and isRealValue(rightName) then
local wrap = html.create('div')
wrap:addClass('kbft-couple')
wrap:node(renderCard(people, leftName, nil, leftClass))
local marriage = wrap:tag('div')
marriage:addClass('kbft-marriage')
if isRealValue(marriageYear) then
marriage:tag('div')
:addClass('kbft-marriage-year')
:wikitext(marriageYear)
end
marriage:tag('div')
:addClass('kbft-marriage-line')
wrap:node(renderCard(people, rightName, nil, rightClass))
return wrap
end
if isRealValue(leftName) then return renderSingleCard(people, leftName, leftClass) end
if isRealValue(rightName) then return renderSingleCard(people, rightName, rightClass) end
return nil
end
local function renderGenerationRow(units)
local row = html.create('div')
row:addClass('kbft-row')
for _, unit in ipairs(units) do
if unit then row:node(unit) end
end
return row
end
local function renderChildCards(people, children)
local childrenWrap = html.create('div')
childrenWrap:addClass('kbft-children')
for _, child in ipairs(children) do
childrenWrap:node(renderCard(people, child.name, relationshipBadge(child.relationshipType)))
end
return childrenWrap
end
local function renderFamilyUnit(people, root, group)
local unit = html.create('div')
unit:addClass('kbft-sibling-unit')
local top = unit:tag('div')
top:addClass('kbft-family-main-wrap')
if isRealValue(group.partner) then
local union = findUnionBetween(people, root, group.partner)
local marriageYear = getMarriageYear(union)
top:node(renderCouple(people, root, group.partner, marriageYear))
else
top:node(renderSingleCard(people, root))
end
if #group.children > 0 then
unit:tag('div'):addClass('kbft-child-down')
unit:node(renderChildCards(people, group.children))
end
return unit
end
local function renderUpperCoupleGeneration(people, couples)
if #couples == 0 then return nil end
local gen = html.create('div')
gen:addClass('kbft-generation')
local units = {}
for _, pair in ipairs(couples) do
table.insert(units, renderCouple(people, pair[1], pair[2], nil))
end
gen:node(renderGenerationRow(units))
return gen
end
local function buildGrandparentCouples(people, root)
local parents = getParents(people, root)
local couples = {}
for _, parentName in ipairs(parents) do
local parent = people[parentName]
if parent then
local gp = uniq(parent.parents)
sortNames(people, gp)
if #gp > 0 then
table.insert(couples, { gp[1], gp[2] })
end
end
end
return couples
end
local function buildParentCouples(people, root)
local parents = getParents(people, root)
if #parents == 0 then return {} end
return { { parents[1], parents[2] } }
end
local function renderFocalGeneration(people, root)
local sequence = getRootSiblingSequence(people, root)
local gen = html.create('div')
gen:addClass('kbft-generation')
local groupWrap = gen:tag('div')
groupWrap:addClass('kbft-siblings')
groupWrap:tag('div')
:addClass('kbft-sibling-spine')
local row = groupWrap:tag('div')
row:addClass('kbft-sibling-row')
for _, name in ipairs(sequence) do
local unit = row:tag('div')
unit:addClass('kbft-sibling-unit')
unit:tag('div')
:addClass('kbft-sibling-up')
if name == root then
unit:node(renderSingleCard(people, name, 'kbft-focus-card'))
else
unit:node(renderSingleCard(people, name))
end
end
return gen
end
local function renderFamilyGroupsGeneration(people, root)
local groups = getFamilyGroupsForRoot(people, root)
if #groups == 0 then return nil end
local gen = html.create('div')
gen:addClass('kbft-generation')
local units = {}
for _, group in ipairs(groups) do
table.insert(units, renderFamilyUnit(people, root, group))
end
gen:node(renderGenerationRow(units))
return gen
end
-- Public renderers
local function renderConnectedForRoot(people, root)
local person = people[root]
if not person then
return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(root) .. '".'
end
local connected = getConnectedPeople(people, root)
local node = html.create('div')
node:addClass('kbft-tree')
node:tag('div')
:addClass('kbft-title')
:wikitext('Connected to ' .. makeLink(person.name, person.displayName))
local gen = node:tag('div')
gen:addClass('kbft-generation')
local units = {}
for _, name in ipairs(connected) do
table.insert(units, renderSingleCard(people, name))
end
gen:node(renderGenerationRow(units))
return tostring(node)
end
local function renderProfileForRoot(people, root)
local person = people[root]
if not person then
return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(root) .. '".'
end
local node = html.create('div')
node:addClass('kbft-tree')
node:tag('div')
:addClass('kbft-title')
:wikitext(makeLink(person.name, person.displayName))
local function addSection(label, names)
names = uniq(names)
if #names == 0 then return end
sortNames(people, names)
node:tag('div')
:addClass('kbft-title')
:css('margin-top', '22px')
:wikitext(label)
local gen = node:tag('div')
gen:addClass('kbft-generation')
local units = {}
for _, name in ipairs(names) do
table.insert(units, renderSingleCard(people, name))
end
gen:node(renderGenerationRow(units))
end
addSection('Parents', person.parents)
addSection('Partners', person.partners)
addSection('Children', person.children)
return tostring(node)
end
local function renderTreeForRoot(people, root)
local person = people[root]
if not person then
return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(root) .. '".'
end
local node = html.create('div')
node:addClass('kbft-tree')
node:tag('div')
:addClass('kbft-title')
:wikitext('Family Tree: ' .. makeLink(person.name, person.displayName))
local gpGen = renderUpperCoupleGeneration(people, buildGrandparentCouples(people, root))
if gpGen then
node:node(gpGen)
node:tag('div'):addClass('kbft-connector')
end
local parentGen = renderUpperCoupleGeneration(people, buildParentCouples(people, root))
if parentGen then
node:node(parentGen)
node:tag('div'):addClass('kbft-connector')
end
node:node(renderFocalGeneration(people, root))
local familyGen = renderFamilyGroupsGeneration(people, root)
if familyGen then
node:tag('div'):addClass('kbft-connector')
node:node(familyGen)
end
return tostring(node)
end
-- Public functions
function p.tree(frame)
local root = getRoot(frame)
local people = loadData()
return renderTreeForRoot(people, root)
end
function p.profile(frame)
local root = getRoot(frame)
local people = loadData()
return renderProfileForRoot(people, root)
end
function p.connected(frame)
local root = getRoot(frame)
local people = loadData()
return renderConnectedForRoot(people, root)
end
return p