|
|
| Line 1: |
Line 1: |
| local p = {} | | local p = {} |
|
| |
| local cargo = mw.ext.cargo
| |
|
| |
| local function esc(value)
| |
| if not value then
| |
| return ''
| |
| end
| |
| value = tostring(value)
| |
| value = value:gsub('\\', '\\\\')
| |
| value = value:gsub('"', '\\"')
| |
| return value
| |
| end
| |
|
| |
| local function cargoQuery(tables, fields, args)
| |
| args = args or {}
| |
| local ok, result = pcall(function()
| |
| return cargo.query(tables, fields, args)
| |
| end)
| |
|
| |
| if ok and result then
| |
| return result
| |
| end
| |
|
| |
| return {}
| |
| end
| |
|
| |
| 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 formatYear(dateValue)
| |
| dateValue = trim(dateValue)
| |
| if not dateValue then
| |
| return nil
| |
| end
| |
| return tostring(dateValue):match('^(%d%d%d%d)')
| |
| end
| |
|
| |
| local function addUnique(list, seen, value)
| |
| value = trim(value)
| |
| if value and not seen[value] then
| |
| seen[value] = true
| |
| table.insert(list, value)
| |
| end
| |
| end
| |
|
| |
| local function addSet(set, value)
| |
| value = trim(value)
| |
| if value then
| |
| set[value] = true
| |
| end
| |
| end
| |
|
| |
| local function sorted(list)
| |
| table.sort(list, function(a, b)
| |
| return tostring(a):lower() < tostring(b):lower()
| |
| end)
| |
| return list
| |
| end
| |
|
| |
| local function getCharacter(pageName)
| |
| pageName = trim(pageName)
| |
| if not pageName then
| |
| return nil
| |
| end
| |
|
| |
| local rows = cargoQuery(
| |
| 'Characters',
| |
| 'Page,DisplayName,BirthDate,DeathDate,Status,Gender',
| |
| {
| |
| where = 'Page="' .. esc(pageName) .. '"',
| |
| limit = 1
| |
| }
| |
| )
| |
|
| |
| return rows[1]
| |
| end
| |
|
| |
| local function getDisplayName(pageName)
| |
| local c = getCharacter(pageName)
| |
| if c and trim(c.DisplayName) then
| |
| return trim(c.DisplayName)
| |
| end
| |
| return pageName
| |
| end
| |
|
| |
| local function makeLinkedName(pageName)
| |
| return '[[' .. pageName .. '|' .. getDisplayName(pageName) .. ']]'
| |
| end
| |
|
| |
| local function linkList(list)
| |
| local out = {}
| |
| for _, pageName in ipairs(list or {}) do
| |
| table.insert(out, makeLinkedName(pageName))
| |
| end
| |
| return table.concat(out, '<br>')
| |
| end
| |
|
| |
| local function getParents(person)
| |
| person = trim(person)
| |
| if not person then
| |
| return {}, nil
| |
| end
| |
|
| |
| local rows = cargoQuery(
| |
| 'ParentChild',
| |
| 'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
| |
| {
| |
| where = 'Child="' .. esc(person) .. '"',
| |
| limit = 20
| |
| }
| |
| )
| |
|
| |
| local parents = {}
| |
| local seen = {}
| |
|
| |
| for _, row in ipairs(rows) do
| |
| addUnique(parents, seen, row.Parent1)
| |
| addUnique(parents, seen, row.Parent2)
| |
| end
| |
|
| |
| return sorted(parents), rows[1]
| |
| end
| |
|
| |
| local function getChildren(person)
| |
| person = trim(person)
| |
| if not person then
| |
| return {}
| |
| end
| |
|
| |
| local rows = cargoQuery(
| |
| 'ParentChild',
| |
| 'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
| |
| {
| |
| where = 'Parent1="' .. esc(person) .. '" OR Parent2="' .. esc(person) .. '"',
| |
| limit = 200
| |
| }
| |
| )
| |
|
| |
| table.sort(rows, function(a, b)
| |
| local aOrder = tonumber(a.BirthOrder) or 9999
| |
| local bOrder = tonumber(b.BirthOrder) or 9999
| |
| if aOrder == bOrder then
| |
| return tostring(a.Child):lower() < tostring(b.Child):lower()
| |
| end
| |
| return aOrder < bOrder
| |
| end)
| |
|
| |
| local children = {}
| |
| local seen = {}
| |
|
| |
| for _, row in ipairs(rows) do
| |
| addUnique(children, seen, row.Child)
| |
| end
| |
|
| |
| return children
| |
| end
| |
|
| |
| local function getPartners(person)
| |
| person = trim(person)
| |
| if not person then
| |
| return {}, {}
| |
| end
| |
|
| |
| local rows = cargoQuery(
| |
| 'Unions',
| |
| 'UnionID,Partner1,Partner2,UnionType,Status,MarriageDate,DivorceDate,EngagementDate',
| |
| {
| |
| where = 'Partner1="' .. esc(person) .. '" OR Partner2="' .. esc(person) .. '"',
| |
| limit = 50
| |
| }
| |
| )
| |
|
| |
| local partners = {}
| |
| local seen = {}
| |
|
| |
| for _, row in ipairs(rows) do
| |
| local p1 = trim(row.Partner1)
| |
| local p2 = trim(row.Partner2)
| |
|
| |
| if p1 == person then
| |
| addUnique(partners, seen, p2)
| |
| elseif p2 == person then
| |
| addUnique(partners, seen, p1)
| |
| end
| |
| end
| |
|
| |
| return sorted(partners), rows
| |
| end
| |
|
| |
| local function getUnionBetween(personA, personB)
| |
| personA = trim(personA)
| |
| personB = trim(personB)
| |
|
| |
| if not personA or not personB then
| |
| return nil
| |
| end
| |
|
| |
| local rows = cargoQuery(
| |
| 'Unions',
| |
| 'UnionID,Partner1,Partner2,UnionType,Status,MarriageDate,DivorceDate,EngagementDate',
| |
| {
| |
| where = '(Partner1="' .. esc(personA) .. '" AND Partner2="' .. esc(personB) .. '") OR (Partner1="' .. esc(personB) .. '" AND Partner2="' .. esc(personA) .. '")',
| |
| limit = 1
| |
| }
| |
| )
| |
|
| |
| return rows[1]
| |
| end
| |
|
| |
| local function getSiblingGeneration(person)
| |
| person = trim(person)
| |
| if not person then
| |
| return {}
| |
| end
| |
|
| |
| local targetRows = cargoQuery(
| |
| 'ParentChild',
| |
| 'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
| |
| {
| |
| where = 'Child="' .. esc(person) .. '"',
| |
| limit = 10
| |
| }
| |
| )
| |
|
| |
| if not targetRows[1] then
| |
| return { person }
| |
| end
| |
|
| |
| local targetUnion = trim(targetRows[1].UnionID)
| |
| local targetP1 = trim(targetRows[1].Parent1)
| |
| local targetP2 = trim(targetRows[1].Parent2)
| |
|
| |
| local whereParts = {}
| |
| if targetUnion then
| |
| table.insert(whereParts, 'UnionID="' .. esc(targetUnion) .. '"')
| |
| end
| |
| if targetP1 and targetP2 then
| |
| table.insert(whereParts, '(Parent1="' .. esc(targetP1) .. '" AND Parent2="' .. esc(targetP2) .. '")')
| |
| end
| |
|
| |
| if #whereParts == 0 then
| |
| return { person }
| |
| end
| |
|
| |
| local rows = cargoQuery(
| |
| 'ParentChild',
| |
| 'Child,Parent1,Parent2,UnionID,RelationshipType,BirthOrder',
| |
| {
| |
| where = table.concat(whereParts, ' OR '),
| |
| limit = 100
| |
| }
| |
| )
| |
|
| |
| table.sort(rows, function(a, b)
| |
| local aOrder = tonumber(a.BirthOrder) or 9999
| |
| local bOrder = tonumber(b.BirthOrder) or 9999
| |
| if aOrder == bOrder then
| |
| return tostring(a.Child):lower() < tostring(b.Child):lower()
| |
| end
| |
| return aOrder < bOrder
| |
| end)
| |
|
| |
| local people = {}
| |
| local seen = {}
| |
| for _, row in ipairs(rows) do
| |
| addUnique(people, seen, row.Child)
| |
| end
| |
|
| |
| if #people == 0 then
| |
| table.insert(people, person)
| |
| end
| |
|
| |
| return people
| |
| end
| |
|
| |
| local function buildCoupleGroups(people)
| |
| local groups = {}
| |
| local used = {}
| |
| local working = {}
| |
|
| |
| for _, person in ipairs(people or {}) do
| |
| table.insert(working, person)
| |
| end
| |
| sorted(working)
| |
|
| |
| for _, person in ipairs(working) do
| |
| if not used[person] then
| |
| local partners = getPartners(person)
| |
| local matchedPartner = nil
| |
|
| |
| for _, partner in ipairs(partners) do
| |
| for _, candidate in ipairs(working) do
| |
| if candidate == partner and not used[candidate] then
| |
| matchedPartner = partner
| |
| break
| |
| end
| |
| end
| |
| if matchedPartner then
| |
| break
| |
| end
| |
| end
| |
|
| |
| if matchedPartner then
| |
| used[person] = true
| |
| used[matchedPartner] = true
| |
|
| |
| local union = getUnionBetween(person, matchedPartner)
| |
| table.insert(groups, {
| |
| type = 'couple',
| |
| left = person,
| |
| right = matchedPartner,
| |
| marriageYear = union and formatYear(union.MarriageDate) or nil
| |
| })
| |
| else
| |
| used[person] = true
| |
| table.insert(groups, {
| |
| type = 'single',
| |
| person = person
| |
| })
| |
| end
| |
| end
| |
| end
| |
|
| |
| return groups
| |
| end
| |
|
| |
| local function buildFamilyUnitsForGeneration(people)
| |
| local units = {}
| |
|
| |
| for _, person in ipairs(people or {}) do
| |
| local partners = getPartners(person)
| |
| local children = getChildren(person)
| |
|
| |
| local partner = partners[1]
| |
| local marriageYear = nil
| |
|
| |
| if partner then
| |
| local union = getUnionBetween(person, partner)
| |
| marriageYear = union and formatYear(union.MarriageDate) or nil
| |
| end
| |
|
| |
| table.insert(units, {
| |
| person = person,
| |
| partner = partner,
| |
| marriageYear = marriageYear,
| |
| children = children
| |
| })
| |
| end
| |
|
| |
| return units
| |
| end
| |
|
| |
| local function makeCard(pageName)
| |
| pageName = trim(pageName)
| |
| if not pageName then
| |
| return ''
| |
| end
| |
|
| |
| local c = getCharacter(pageName)
| |
| local displayName = getDisplayName(pageName)
| |
| local birthYear = c and formatYear(c.BirthDate) or nil
| |
| local deathYear = c and formatYear(c.DeathDate) or nil
| |
|
| |
| local years = ''
| |
| if birthYear or deathYear then
| |
| years = '<div class="ft-years">' .. (birthYear or '?') .. '–' .. (deathYear or '') .. '</div>'
| |
| end
| |
|
| |
| return '<div class="ft-card">[[' .. pageName .. '|' .. displayName .. ']]' .. years .. '</div>'
| |
| end
| |
|
| |
| local function makeCoupleMarkup(left, right, marriageYear)
| |
| if right then
| |
| local html = {}
| |
| table.insert(html, '<div class="ft-unit ft-unit-couple">')
| |
| table.insert(html, makeCard(left))
| |
| table.insert(html, '<div class="ft-marriage">')
| |
| if marriageYear then
| |
| table.insert(html, '<div class="ft-marriage-year">' .. marriageYear .. '</div>')
| |
| end
| |
| table.insert(html, '<div class="ft-marriage-line"></div>')
| |
| table.insert(html, '</div>')
| |
| table.insert(html, makeCard(right))
| |
| table.insert(html, '</div>')
| |
| return table.concat(html)
| |
| else
| |
| return '<div class="ft-unit ft-unit-single">' .. makeCard(left) .. '</div>'
| |
| end
| |
| end
| |
|
| |
| local function makeCoupleGroupRow(groups)
| |
| if not groups or #groups == 0 then
| |
| return ''
| |
| end
| |
|
| |
| local html = {}
| |
| table.insert(html, '<div class="ft-row">')
| |
|
| |
| for _, group in ipairs(groups) do
| |
| if group.type == 'single' then
| |
| table.insert(html, makeCoupleMarkup(group.person, nil, nil))
| |
| else
| |
| table.insert(html, makeCoupleMarkup(group.left, group.right, group.marriageYear))
| |
| end
| |
| end
| |
|
| |
| table.insert(html, '</div>')
| |
| return table.concat(html)
| |
| end
| |
|
| |
| local function makeFamilyUnitsRow(units)
| |
| if not units or #units == 0 then
| |
| return ''
| |
| end
| |
|
| |
| local html = {}
| |
| table.insert(html, '<div class="ft-sibling-generation">')
| |
| table.insert(html, '<div class="ft-sibling-spine"></div>')
| |
| table.insert(html, '<div class="ft-family-row">')
| |
|
| |
| for _, unit in ipairs(units) do
| |
| table.insert(html, '<div class="ft-family-unit">')
| |
| table.insert(html, '<div class="ft-family-up-line"></div>')
| |
| table.insert(html, '<div class="ft-family-main">')
| |
| table.insert(html, makeCoupleMarkup(unit.person, unit.partner, unit.marriageYear))
| |
| table.insert(html, '</div>')
| |
|
| |
| if unit.children and #unit.children > 0 then
| |
| table.insert(html, '<div class="ft-family-desc-line"></div>')
| |
| table.insert(html, '<div class="ft-family-children">')
| |
| for _, child in ipairs(unit.children) do
| |
| table.insert(html, makeCard(child))
| |
| end
| |
| table.insert(html, '</div>')
| |
| end
| |
|
| |
| table.insert(html, '</div>')
| |
| end
| |
|
| |
| table.insert(html, '</div>')
| |
| table.insert(html, '</div>')
| |
| return table.concat(html)
| |
| end
| |
|
| |
| function p.connected(frame)
| |
| local args = frame.args
| |
| local parentArgs = frame:getParent() and frame:getParent().args or {}
| |
| local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])
| |
|
| |
| if not root then
| |
| return 'Error: no root provided. Use root=Character Name'
| |
| end
| |
|
| |
| local visited = {}
| |
| local queue = {}
| |
| local head = 1
| |
|
| |
| visited[root] = true
| |
| table.insert(queue, root)
| |
|
| |
| while head <= #queue do
| |
| local current = queue[head]
| |
| head = head + 1
| |
|
| |
| local neighbors = {}
| |
| local parents = getParents(current)
| |
| local children = getChildren(current)
| |
| local partners = getPartners(current)
| |
|
| |
| for _, person in ipairs(parents) do
| |
| addSet(neighbors, person)
| |
| end
| |
| for _, person in ipairs(children) do
| |
| addSet(neighbors, person)
| |
| end
| |
| for _, person in ipairs(partners) do
| |
| addSet(neighbors, person)
| |
| end
| |
|
| |
| for neighbor, _ in pairs(neighbors) do
| |
| if neighbor and not visited[neighbor] then
| |
| visited[neighbor] = true
| |
| table.insert(queue, neighbor)
| |
| end
| |
| end
| |
| end
| |
|
| |
| local people = {}
| |
| for name, _ in pairs(visited) do
| |
| table.insert(people, name)
| |
| end
| |
| sorted(people)
| |
|
| |
| local lines = {}
| |
| table.insert(lines, "'''Connected component for " .. getDisplayName(root) .. "'''")
| |
| table.insert(lines, '* Total people found: ' .. tostring(#people))
| |
| for _, person in ipairs(people) do
| |
| table.insert(lines, '* ' .. makeLinkedName(person))
| |
| end
| |
|
| |
| return table.concat(lines, '\n')
| |
| end
| |
|
| |
| function p.profile(frame)
| |
| local args = frame.args
| |
| local parentArgs = frame:getParent() and frame:getParent().args or {}
| |
| local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])
| |
|
| |
| if not root then
| |
| return 'Error: no root provided. Use root=Character Name'
| |
| end
| |
|
| |
| local parents = getParents(root)
| |
| local siblings = getSiblingGeneration(root)
| |
| local partners = getPartners(root)
| |
| local children = getChildren(root)
| |
|
| |
| local siblingList = {}
| |
| for _, person in ipairs(siblings) do
| |
| if person ~= root then
| |
| table.insert(siblingList, person)
| |
| end
| |
| end
| |
|
| |
| local lines = {}
| |
| table.insert(lines, '{| class="wikitable" style="width:100%; max-width:900px;"')
| |
| table.insert(lines, '|-')
| |
| table.insert(lines, '! colspan="2" | Family profile for ' .. getDisplayName(root))
| |
| table.insert(lines, '|-')
| |
| table.insert(lines, '! style="width:20%;" | Person')
| |
| table.insert(lines, '| ' .. makeLinkedName(root))
| |
|
| |
| table.insert(lines, '|-')
| |
| table.insert(lines, '! Parents')
| |
| table.insert(lines, '| ' .. (#parents > 0 and linkList(parents) or '—'))
| |
|
| |
| table.insert(lines, '|-')
| |
| table.insert(lines, '! Siblings')
| |
| table.insert(lines, '| ' .. (#siblingList > 0 and linkList(siblingList) or '—'))
| |
|
| |
| table.insert(lines, '|-')
| |
| table.insert(lines, '! Partners')
| |
| table.insert(lines, '| ' .. (#partners > 0 and linkList(partners) or '—'))
| |
|
| |
| table.insert(lines, '|-')
| |
| table.insert(lines, '! Children')
| |
| table.insert(lines, '| ' .. (#children > 0 and linkList(children) or '—'))
| |
|
| |
| table.insert(lines, '|}')
| |
|
| |
| return table.concat(lines, '\n')
| |
| end
| |
|
| |
|
| function p.tree(frame) | | function p.tree(frame) |
| local args = frame.args
| | local root = frame.args.root or frame.args[1] |
| local parentArgs = frame:getParent() and frame:getParent().args or {}
| | if not root then |
| local root = trim(args.root or parentArgs.root or args[1] or parentArgs[1])
| | return "Error: no root provided." |
| | | end |
| if not root then
| |
| return 'Error: no root provided. Use root=Character Name'
| |
| end
| |
| | |
| local parents = getParents(root)
| |
| local siblingGeneration = getSiblingGeneration(root)
| |
| | |
| local grandparents = {}
| |
| local gpSeen = {}
| |
| for _, parentName in ipairs(parents) do
| |
| local parentParents = getParents(parentName)
| |
| for _, gp in ipairs(parentParents) do
| |
| addUnique(grandparents, gpSeen, gp)
| |
| end
| |
| end
| |
| sorted(grandparents)
| |
| | |
| local grandparentGroups = buildCoupleGroups(grandparents)
| |
| local parentGroups = buildCoupleGroups(parents)
| |
| local familyUnits = buildFamilyUnitsForGeneration(siblingGeneration)
| |
| | |
| local html = {}
| |
| table.insert(html, '<div class="ft-tree">')
| |
| table.insert(html, '<div class="ft-title">Family tree for ' .. getDisplayName(root) .. '</div>')
| |
| | |
| if #grandparents > 0 then
| |
| table.insert(html, '<div class="ft-generation">')
| |
| table.insert(html, makeCoupleGroupRow(grandparentGroups))
| |
| table.insert(html, '</div>')
| |
| end
| |
| | |
| if #grandparents > 0 and #parents > 0 then
| |
| table.insert(html, '<div class="ft-connector"></div>')
| |
| end
| |
| | |
| if #parents > 0 then
| |
| table.insert(html, '<div class="ft-generation">')
| |
| table.insert(html, makeCoupleGroupRow(parentGroups))
| |
| table.insert(html, '</div>')
| |
| end
| |
| | |
| if #parents > 0 and #familyUnits > 0 then
| |
| table.insert(html, '<div class="ft-connector"></div>')
| |
| end
| |
| | |
| if #familyUnits > 0 then
| |
| table.insert(html, '<div class="ft-generation">')
| |
| table.insert(html, makeFamilyUnitsRow(familyUnits))
| |
| table.insert(html, '</div>')
| |
| end
| |
|
| |
|
| table.insert(html, '</div>')
| | return '<div style="padding:20px; border:3px solid red; font-size:20px;">NEW TREE MODULE LOADED: ' .. root .. '</div>' |
| return table.concat(html)
| |
| end | | end |
|
| |
|
| return p | | return p |