Module:FamilyTree: Difference between revisions

From KB Lexicon
No edit summary
No edit summary
Line 79: Line 79:
return nil
return nil
end
end
local function getPeopleField(row, keys)
for _, key in ipairs(keys) do
if row[key] ~= nil then
return trim(row[key])
end
end
return nil
end
-- =========================================
-- Data loading
-- =========================================


local function ensurePerson(people, name)
local function ensurePerson(people, name)
Line 106: Line 93:
birthDate = nil,
birthDate = nil,
deathDate = nil,
deathDate = nil,
status = nil,
birthFamily = nil,
birthFamily = nil,
currentFamily = nil,
currentFamily = nil,
status = nil,
father = nil,
mother = nil,
adoptiveFather = nil,
adoptiveMother = nil,
bloodStatus = nil,
title = nil,
heir = nil,
illegitimate = nil,
adopted = nil,
parents = {},
parents = {},
children = {},
children = {},
Line 118: Line 114:
return people[name]
return people[name]
end
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 function queryCharacters()
-- Pull all columns; we map flexibly in case your exact field names vary.
local results = cargo.query(
local results = cargo.query(
'Characters',
'Characters',
'_pageName=Page,Name,DisplayName,Gender,BirthDate,DeathDate,BirthFamily,CurrentFamily,Status',
'Page,DisplayName,Gender,BirthDate,DeathDate,Status,BirthFamily,CurrentFamily,Father,Mother,AdoptiveFather,AdoptiveMother,BloodStatus,Title,Heir,Illegitimate,Adopted',
{ limit = 5000 }
{ limit = 5000 }
)
)
Line 130: Line 152:


for _, row in ipairs(results) do
for _, row in ipairs(results) do
local name =
local page = trim(row.Page)
getPeopleField(row, { 'Name', 'Page', '_pageName', 'DisplayName' })


if isRealValue(name) then
if isRealValue(page) then
people[name] = {
people[page] = {
name = name,
name = page,
displayName = getPeopleField(row, { 'DisplayName' }) or name,
displayName = trim(row.DisplayName) or page,
gender = getPeopleField(row, { 'Gender' }),
gender = trim(row.Gender),
birthDate = getPeopleField(row, { 'BirthDate' }),
birthDate = trim(row.BirthDate),
deathDate = getPeopleField(row, { 'DeathDate' }),
deathDate = trim(row.DeathDate),
birthFamily = getPeopleField(row, { 'BirthFamily' }),
status = trim(row.Status),
currentFamily = getPeopleField(row, { 'CurrentFamily' }),
birthFamily = trim(row.BirthFamily),
status = getPeopleField(row, { 'Status' }),
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 = {},
parents = {},
children = {},
children = {},
Line 180: Line 210:
addUnique(people[p2].children, child)
addUnique(people[p2].children, child)
end
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
end
Line 187: Line 245:
local results = cargo.query(
local results = cargo.query(
'Unions',
'Unions',
'UnionID,Partner1,Partner2,UnionType,Status,EngagementDate,MarriageDate,DivorceDate',
'UnionID,Partner1,Partner2,UnionType,Status,StartDate,EndDate,MarriageDate,DivorceDate,EngagementDate',
{ limit = 5000 }
{ limit = 5000 }
)
)
Line 196: Line 254:
local p2 = trim(row.Partner2)
local p2 = trim(row.Partner2)


if isRealValue(p1) then ensurePerson(people, p1) end
if isRealValue(p1) then
if isRealValue(p2) then ensurePerson(people, p2) end
ensurePerson(people, p1)
end
if isRealValue(p2) then
ensurePerson(people, p2)
end


if isRealValue(p1) and isRealValue(p2) then
if isRealValue(p1) and isRealValue(p2) then
Line 208: Line 270:
unionType = trim(row.UnionType),
unionType = trim(row.UnionType),
status = trim(row.Status),
status = trim(row.Status),
engagementDate = trim(row.EngagementDate),
startDate = trim(row.StartDate),
endDate = trim(row.EndDate),
marriageDate = trim(row.MarriageDate),
marriageDate = trim(row.MarriageDate),
divorceDate = trim(row.DivorceDate)
divorceDate = trim(row.DivorceDate),
engagementDate = trim(row.EngagementDate)
})
})


Line 218: Line 282:
unionType = trim(row.UnionType),
unionType = trim(row.UnionType),
status = trim(row.Status),
status = trim(row.Status),
engagementDate = trim(row.EngagementDate),
startDate = trim(row.StartDate),
endDate = trim(row.EndDate),
marriageDate = trim(row.MarriageDate),
marriageDate = trim(row.MarriageDate),
divorceDate = trim(row.DivorceDate)
divorceDate = trim(row.DivorceDate),
engagementDate = trim(row.EngagementDate)
})
})
end
end
Line 226: Line 292:
end
end


local function loadData()
local function finalizePeople(people)
local people = queryCharacters()
loadParentChild(people)
loadUnions(people)
 
for _, person in pairs(people) do
for _, person in pairs(people) do
person.parents = uniq(person.parents)
person.parents = uniq(person.parents)
Line 236: Line 298:
person.partners = uniq(person.partners)
person.partners = uniq(person.partners)
end
end
end


local function loadData()
local people = queryCharacters()
loadParentChild(people)
loadCharacterParentFallbacks(people)
loadUnions(people)
finalizePeople(people)
return people
return people
end
end
Line 243: Line 312:
-- Relationship helpers
-- Relationship helpers
-- =========================================
-- =========================================
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 getGrandparents(people, personName)
local function getGrandparents(people, personName)
Line 316: Line 377:


-- =========================================
-- =========================================
-- Rendering
-- Rendering helpers
-- =========================================
-- =========================================


Line 464: Line 525:


addField('Display Name', person.displayName ~= person.name and person.displayName or nil)
addField('Display Name', person.displayName ~= person.name and person.displayName or nil)
addField('Title', person.title)
addField('Gender', person.gender)
addField('Gender', person.gender)
addField('Birth Date', person.birthDate)
addField('Birth Date', person.birthDate)
addField('Death Date', person.deathDate)
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('Birth Family', person.birthFamily and makeLink(person.birthFamily) or nil)
addField('Current Family', person.currentFamily and makeLink(person.currentFamily) or nil)
addField('Current Family', person.currentFamily and makeLink(person.currentFamily) or nil)
addField('Status', person.status)
addField('Heir', yesNo(person.heir))
addField('Illegitimate', yesNo(person.illegitimate))
addField('Adopted', yesNo(person.adopted))


if #person.parents > 0 then
if #person.parents > 0 then

Revision as of 21:38, 29 March 2026

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