Module:FamilyTree: Difference between revisions

From KB Lexicon
No edit summary
No edit summary
Line 25: Line 25:
return false
return false
end
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 45: Line 44:
local out = {}
local out = {}
local seen = {}
local 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 52: Line 50:
end
end
end
end
return out
return out
end
local function makeLink(name)
if not isRealValue(name) then
return ''
end
return string.format('[[%s|%s]]', name, name)
end
end


Line 82: Line 72:
local function getRoot(frame)
local function getRoot(frame)
return getArg(frame, 'root') or getArg(frame, 'person') or mw.title.getCurrentTitle().text
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
end


Line 281: Line 279:


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


local function renderPersonBox(name, extraClass)
local function renderCard(people, name)
if not isRealValue(name) then
if not isRealValue(name) then
return nil
return nil
end
end


local box = html.create('div')
local person = people[name] or { name = name, displayName = name }
box:addClass('familytree-person')
 
if isRealValue(extraClass) then
local card = html.create('div')
box:addClass(extraClass)
card:addClass('kbft-card')
end
card:wikitext(makeLink(person.name, person.displayName))
box:wikitext(makeLink(name))
return card
return box
end
end


local function renderRow(label, names, rowClass)
local function renderSingle(people, name)
names = uniq(names)
local wrap = html.create('div')
if #names == 0 then
wrap:addClass('kbft-single')
return nil
wrap:node(renderCard(people, name))
end
return wrap
end


local function renderRow(people, names)
local row = html.create('div')
local row = html.create('div')
row:addClass('familytree-row')
row:addClass('kbft-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
for _, name in ipairs(names) do
local node = renderPersonBox(name)
row:node(renderSingle(people, name))
if node then
items:node(node)
end
end
end


Line 327: Line 313:
end
end


local function renderTreeForRoot(people, root)
-- =========================================
-- Public renderers
-- =========================================
 
local function renderConnectedForRoot(people, root)
local person = people[root]
local person = people[root]
if not person then
if not person then
Line 333: Line 323:
end
end


local grandparents = getGrandparents(people, root)
local connected = getConnectedPeople(people, root)
local parents = uniq(person.parents)
sortNames(people, connected)
local siblings = getSiblings(people, root)
local partners = uniq(person.partners)
local children = uniq(person.children)


sortNames(people, grandparents)
local node = html.create('div')
sortNames(people, parents)
node:addClass('kbft-tree')
sortNames(people, siblings)
sortNames(people, partners)
sortNames(people, children)


local rootNode = html.create('div')
node:tag('div')
rootNode:addClass('familytree-wrapper')
:addClass('kbft-title')
:wikitext('Connected to ' .. makeLink(person.name, person.displayName))


local gpRow = renderRow('Grandparents', grandparents, 'familytree-grandparents')
local row = node:tag('div')
if gpRow then rootNode:node(gpRow) end
row:addClass('kbft-row')


local pRow = renderRow('Parents', parents, 'familytree-parents')
for _, name in ipairs(connected) do
if pRow then rootNode:node(pRow) end
row:node(renderSingle(people, name))
end


local selfRow = html.create('div')
return tostring(node)
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
end


Line 386: Line 349:
end
end


local rootNode = html.create('div')
local node = html.create('div')
rootNode:addClass('familytree-profile')
node:addClass('kbft-tree')


rootNode:tag('div')
node:tag('div')
:addClass('familytree-profile-name')
:addClass('kbft-title')
:wikitext(makeLink(person.name))
:wikitext(makeLink(person.name, person.displayName))


local dl = rootNode:tag('dl')
local function addSection(label, values)
 
values = uniq(values)
local function addField(label, value)
if #values == 0 then
if isRealValue(value) then
return
dl:tag('dt'):wikitext(label)
dl:tag('dd'):wikitext(value)
end
end
end


addField('Display Name', person.displayName ~= person.name and person.displayName or nil)
node:tag('div')
:addClass('kbft-generation')
:tag('div')
:addClass('kbft-title')
:wikitext(label)


if #person.parents > 0 then
node:node(renderRow(people, values))
local links = {}
for _, name in ipairs(person.parents) do
table.insert(links, makeLink(name))
end
addField('Parents', table.concat(links, ', '))
end
end


if #person.partners > 0 then
addSection('Parents', person.parents)
local links = {}
addSection('Partners', person.partners)
for _, name in ipairs(person.partners) do
addSection('Children', person.children)
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)
return tostring(node)
end
end


local function renderConnectedForRoot(people, root)
local function renderTreeForRoot(people, root)
local person = people[root]
local person = people[root]
if not person then
if not person then
Line 437: Line 384:
end
end


local connected = getConnectedPeople(people, root)
local grandparents = getGrandparents(people, root)
sortNames(people, connected)
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 node = html.create('div')
node:addClass('kbft-tree')
 
node:tag('div')
:addClass('kbft-title')
:wikitext('Family Tree: ' .. makeLink(person.name, person.displayName))
 
if #grandparents > 0 then
node:tag('div'):addClass('kbft-generation'):node(renderRow(people, grandparents))
node:tag('div'):addClass('kbft-connector')
end
 
if #parents > 0 then
node:tag('div'):addClass('kbft-generation'):node(renderRow(people, parents))
node:tag('div'):addClass('kbft-connector')
end


local rootNode = html.create('div')
node:tag('div'):addClass('kbft-generation'):node(renderRow(people, { root }))
rootNode:addClass('familytree-connected')


rootNode:tag('div')
if #siblings > 0 then
:addClass('familytree-connected-title')
node:tag('div'):addClass('kbft-connector')
:wikitext('Connected to ' .. makeLink(root))
node:tag('div'):addClass('kbft-generation'):node(renderRow(people, siblings))
end


local items = rootNode:tag('div')
if #partners > 0 then
:addClass('familytree-row-items')
node:tag('div'):addClass('kbft-connector')
node:tag('div'):addClass('kbft-generation'):node(renderRow(people, partners))
end


for _, name in ipairs(connected) do
if #children > 0 then
items:node(renderPersonBox(name))
node:tag('div'):addClass('kbft-connector')
node:tag('div'):addClass('kbft-generation'):node(renderRow(people, children))
end
end


return tostring(rootNode)
return tostring(node)
end
end



Revision as of 21:47, 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 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 = {}
		}
	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 helpers
-- =========================================

local function renderCard(people, name)
	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')
	card:wikitext(makeLink(person.name, person.displayName))
	return card
end

local function renderSingle(people, name)
	local wrap = html.create('div')
	wrap:addClass('kbft-single')
	wrap:node(renderCard(people, name))
	return wrap
end

local function renderRow(people, names)
	local row = html.create('div')
	row:addClass('kbft-row')

	for _, name in ipairs(names) do
		row:node(renderSingle(people, name))
	end

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

	local node = html.create('div')
	node:addClass('kbft-tree')

	node:tag('div')
		:addClass('kbft-title')
		:wikitext('Connected to ' .. makeLink(person.name, person.displayName))

	local row = node:tag('div')
	row:addClass('kbft-row')

	for _, name in ipairs(connected) do
		row:node(renderSingle(people, name))
	end

	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, values)
		values = uniq(values)
		if #values == 0 then
			return
		end

		node:tag('div')
			:addClass('kbft-generation')
			:tag('div')
			:addClass('kbft-title')
			:wikitext(label)

		node:node(renderRow(people, values))
	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 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 node = html.create('div')
	node:addClass('kbft-tree')

	node:tag('div')
		:addClass('kbft-title')
		:wikitext('Family Tree: ' .. makeLink(person.name, person.displayName))

	if #grandparents > 0 then
		node:tag('div'):addClass('kbft-generation'):node(renderRow(people, grandparents))
		node:tag('div'):addClass('kbft-connector')
	end

	if #parents > 0 then
		node:tag('div'):addClass('kbft-generation'):node(renderRow(people, parents))
		node:tag('div'):addClass('kbft-connector')
	end

	node:tag('div'):addClass('kbft-generation'):node(renderRow(people, { root }))

	if #siblings > 0 then
		node:tag('div'):addClass('kbft-connector')
		node:tag('div'):addClass('kbft-generation'):node(renderRow(people, siblings))
	end

	if #partners > 0 then
		node:tag('div'):addClass('kbft-connector')
		node:tag('div'):addClass('kbft-generation'):node(renderRow(people, partners))
	end

	if #children > 0 then
		node:tag('div'):addClass('kbft-connector')
		node:tag('div'):addClass('kbft-generation'):node(renderRow(people, children))
	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