Module:FamilyTree: Difference between revisions

From KB Lexicon
No edit summary
No edit summary
Line 78: Line 78:


return nil
return nil
end
local function getRoot(frame)
return getArg(frame, 'root') or getArg(frame, 'person') or mw.title.getCurrentTitle().text
end
end


Line 90: Line 94:
name = name,
name = name,
displayName = 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 = {},
parents = {},
children = {},
children = {},
partners = {},
partners = {}
unions = {}
}
}
end
end
Line 121: Line 109:
return mw.ustring.lower(ad) < mw.ustring.lower(bd)
return mw.ustring.lower(ad) < mw.ustring.lower(bd)
end)
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
end


Line 142: Line 115:
-- =========================================
-- =========================================


local function queryCharacters()
local function loadCharacters()
local results = cargo.query(
local results = cargo.query(
'Characters',
'Characters',
'Page,DisplayName,Gender,BirthDate,DeathDate,Status,BirthFamily,CurrentFamily,Father,Mother,AdoptiveFather,AdoptiveMother,BloodStatus,Title,Heir,Illegitimate,Adopted',
'Page,DisplayName',
{ limit = 5000 }
{ limit = 5000 }
)
)
Line 153: Line 126:
for _, row in ipairs(results) do
for _, row in ipairs(results) do
local page = trim(row.Page)
local page = trim(row.Page)
local displayName = trim(row.DisplayName)


if isRealValue(page) then
if isRealValue(page) then
people[page] = {
people[page] = {
name = page,
name = page,
displayName = trim(row.DisplayName) or page,
displayName = 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 = {},
parents = {},
children = {},
children = {},
partners = {},
partners = {}
unions = {}
}
}
end
end
Line 210: Line 168:
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 245: Line 175:
local results = cargo.query(
local results = cargo.query(
'Unions',
'Unions',
'UnionID,Partner1,Partner2,UnionType,Status,StartDate,EndDate,MarriageDate,DivorceDate,EngagementDate',
'UnionID,Partner1,Partner2,UnionType,Status',
{ limit = 5000 }
{ limit = 5000 }
)
)


for _, row in ipairs(results) do
for _, row in ipairs(results) do
local unionID = trim(row.UnionID)
local p1 = trim(row.Partner1)
local p1 = trim(row.Partner1)
local p2 = trim(row.Partner2)
local p2 = trim(row.Partner2)
Line 264: Line 193:
addUnique(people[p1].partners, p2)
addUnique(people[p1].partners, p2)
addUnique(people[p2].partners, p1)
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
end
Line 301: Line 206:


local function loadData()
local function loadData()
local people = queryCharacters()
local people = loadCharacters()
loadParentChild(people)
loadParentChild(people)
loadCharacterParentFallbacks(people)
loadUnions(people)
loadUnions(people)
finalizePeople(people)
finalizePeople(people)
Line 313: Line 217:
-- =========================================
-- =========================================


local function getGrandparents(people, personName)
local function getGrandparents(people, root)
local out = {}
local out = {}
local person = people[personName]
local person = people[root]
if not person then
if not person then
return out
return out
Line 332: Line 236:
end
end


local function getSiblings(people, personName)
local function getSiblings(people, root)
local out = {}
local out = {}
local seen = {}
local seen = {}
local person = people[personName]
local person = people[root]


if not person then
if not person then
Line 345: Line 249:
if parent then
if parent then
for _, childName in ipairs(parent.children) do
for _, childName in ipairs(parent.children) do
if childName ~= personName and not seen[childName] then
if childName ~= root and not seen[childName] then
seen[childName] = true
seen[childName] = true
table.insert(out, childName)
table.insert(out, childName)
Line 356: Line 260:
end
end


local function getConnectedPeople(people, personName)
local function getConnectedPeople(people, root)
local out = {}
local out = {}
local person = people[personName]
local person = people[root]
if not person then
if not person then
return out
return out
Line 367: Line 271:
for _, v in ipairs(person.partners) do addUnique(out, v) end
for _, v in ipairs(person.partners) do addUnique(out, v) end


local siblings = getSiblings(people, personName)
local siblings = getSiblings(people, root)
for _, v in ipairs(siblings) do addUnique(out, v) end
for _, v in ipairs(siblings) do addUnique(out, v) end


local grandparents = getGrandparents(people, personName)
local grandparents = getGrandparents(people, root)
for _, v in ipairs(grandparents) do addUnique(out, v) end
for _, v in ipairs(grandparents) do addUnique(out, v) end


Line 377: Line 281:


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


Line 423: Line 327:
end
end


local function renderTreeForPerson(people, personName)
local function renderTreeForRoot(people, root)
local person = people[personName]
local person = people[root]
if not person then
if not person then
return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(personName) .. '".'
return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(root) .. '".'
end
end


local grandparents = getGrandparents(people, personName)
local grandparents = getGrandparents(people, root)
local parents = uniq(person.parents)
local parents = uniq(person.parents)
local siblings = getSiblings(people, personName)
local siblings = getSiblings(people, root)
local partners = uniq(person.partners)
local partners = uniq(person.partners)
local children = uniq(person.children)
local children = uniq(person.children)
Line 441: Line 345:
sortNames(people, children)
sortNames(people, children)


local root = html.create('div')
local rootNode = html.create('div')
root:addClass('familytree-wrapper')
rootNode:addClass('familytree-wrapper')


local gpRow = renderRow('Grandparents', grandparents, 'familytree-grandparents')
local gpRow = renderRow('Grandparents', grandparents, 'familytree-grandparents')
if gpRow then root:node(gpRow) end
if gpRow then rootNode:node(gpRow) end


local pRow = renderRow('Parents', parents, 'familytree-parents')
local pRow = renderRow('Parents', parents, 'familytree-parents')
if pRow then root:node(pRow) end
if pRow then rootNode:node(pRow) end


local selfRow = html.create('div')
local selfRow = html.create('div')
Line 460: Line 364:
selfRow:tag('div')
selfRow:tag('div')
:addClass('familytree-row-items')
:addClass('familytree-row-items')
:node(renderPersonBox(personName, 'familytree-focus-person'))
:node(renderPersonBox(root, 'familytree-focus-person'))


root:node(selfRow)
rootNode:node(selfRow)


local sRow = renderRow('Siblings', siblings, 'familytree-siblings')
local sRow = renderRow('Siblings', siblings, 'familytree-siblings')
if sRow then root:node(sRow) end
if sRow then rootNode:node(sRow) end


local partnerRow = renderRow('Partners', partners, 'familytree-partners')
local partnerRow = renderRow('Partners', partners, 'familytree-partners')
if partnerRow then root:node(partnerRow) end
if partnerRow then rootNode:node(partnerRow) end


local childRow = renderRow('Children', children, 'familytree-children')
local childRow = renderRow('Children', children, 'familytree-children')
if childRow then root:node(childRow) end
if childRow then rootNode: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)
return tostring(rootNode)
end
end


local function renderProfileForPerson(people, personName)
local function renderProfileForRoot(people, root)
local person = people[personName]
local person = people[root]
if not person then
if not person then
return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(personName) .. '".'
return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(root) .. '".'
end
end


local root = html.create('div')
local rootNode = html.create('div')
root:addClass('familytree-profile')
rootNode:addClass('familytree-profile')


root:tag('div')
rootNode:tag('div')
:addClass('familytree-profile-name')
:addClass('familytree-profile-name')
:wikitext(makeLink(person.name))
:wikitext(makeLink(person.name))


local dl = root:tag('dl')
local dl = rootNode:tag('dl')


local function addField(label, value)
local function addField(label, value)
Line 525: Line 403:


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('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
if #person.parents > 0 then
Line 561: Line 428:
end
end


return tostring(root)
return tostring(rootNode)
end
 
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)
sortNames(people, connected)
 
local rootNode = html.create('div')
rootNode:addClass('familytree-connected')
 
rootNode:tag('div')
:addClass('familytree-connected-title')
:wikitext('Connected to ' .. makeLink(root))
 
local items = rootNode:tag('div')
:addClass('familytree-row-items')
 
for _, name in ipairs(connected) do
items:node(renderPersonBox(name))
end
 
return tostring(rootNode)
end
end


Line 569: Line 462:


function p.tree(frame)
function p.tree(frame)
local personName = getArg(frame, 'person') or mw.title.getCurrentTitle().text
local root = getRoot(frame)
local people = loadData()
local people = loadData()
return renderTreeForPerson(people, personName)
return renderTreeForRoot(people, root)
end
end


function p.profile(frame)
function p.profile(frame)
local personName = getArg(frame, 'person') or mw.title.getCurrentTitle().text
local root = getRoot(frame)
local people = loadData()
local people = loadData()
return renderProfileForPerson(people, personName)
return renderProfileForRoot(people, root)
end
end


function p.connected(frame)
function p.connected(frame)
local personName = getArg(frame, 'person') or mw.title.getCurrentTitle().text
local root = getRoot(frame)
local people = loadData()
local people = loadData()
return renderConnectedForPerson(people, personName)
return renderConnectedForRoot(people, root)
end
end


return p
return p

Revision as of 21:42, 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 getRoot(frame)
	return getArg(frame, 'root') or getArg(frame, 'person') or mw.title.getCurrentTitle().text
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 = {}
		}
	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 = {}
			}
		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 loadUnions(people)
	local results = cargo.query(
		'Unions',
		'UnionID,Partner1,Partner2,UnionType,Status',
		{ 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)
		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 getGrandparents(people, root)
	local out = {}
	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 _, gp in ipairs(parent.parents) do
				addUnique(out, gp)
			end
		end
	end

	return uniq(out)
end

local function getSiblings(people, root)
	local out = {}
	local 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

	return uniq(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

	local siblings = getSiblings(people, root)
	for _, v in ipairs(siblings) do addUnique(out, v) end

	local grandparents = getGrandparents(people, root)
	for _, v in ipairs(grandparents) do addUnique(out, v) end

	return uniq(out)
end

-- =========================================
-- Rendering
-- =========================================

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 renderTreeForRoot(people, root)
	local person = people[root]
	if not person then
		return '<strong>FamilyTree error:</strong> No character found for "' .. tostring(root) .. '".'
	end

	local grandparents = getGrandparents(people, root)
	local parents = uniq(person.parents)
	local siblings = getSiblings(people, root)
	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 rootNode = html.create('div')
	rootNode:addClass('familytree-wrapper')

	local gpRow = renderRow('Grandparents', grandparents, 'familytree-grandparents')
	if gpRow then rootNode:node(gpRow) end

	local pRow = renderRow('Parents', parents, 'familytree-parents')
	if pRow then rootNode: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(root, 'familytree-focus-person'))

	rootNode:node(selfRow)

	local sRow = renderRow('Siblings', siblings, 'familytree-siblings')
	if sRow then rootNode:node(sRow) end

	local partnerRow = renderRow('Partners', partners, 'familytree-partners')
	if partnerRow then rootNode:node(partnerRow) end

	local childRow = renderRow('Children', children, 'familytree-children')
	if childRow then rootNode:node(childRow) end

	return tostring(rootNode)
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 rootNode = html.create('div')
	rootNode:addClass('familytree-profile')

	rootNode:tag('div')
		:addClass('familytree-profile-name')
		:wikitext(makeLink(person.name))

	local dl = rootNode: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)

	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(rootNode)
end

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)
	sortNames(people, connected)

	local rootNode = html.create('div')
	rootNode:addClass('familytree-connected')

	rootNode:tag('div')
		:addClass('familytree-connected-title')
		:wikitext('Connected to ' .. makeLink(root))

	local items = rootNode:tag('div')
		:addClass('familytree-row-items')

	for _, name in ipairs(connected) do
		items:node(renderPersonBox(name))
	end

	return tostring(rootNode)
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