Expand more nativity code aliases (Pal, Col, Arg, …)
Infobox: drop certificate/arrive/depart/years; expand other_class codes
 
(20 intermediate revisions by the same user not shown)
Line 456: Line 456:
EE = 'Estonia',
EE = 'Estonia',
LA = 'Latvia',
LA = 'Latvia',
LAT = 'Latvia',
LI = 'Lithuania',
LI = 'Lithuania',
LT = 'Lithuania',
LT = 'Lithuania',
LV = 'Latvia',
LV = 'Latvia',
AL = 'Albania',
AL = 'Albania',
ALB = 'Albania',
BU = 'Bulgaria',
BU = 'Bulgaria',
BUL = 'Bulgaria',
BG = 'Bulgaria',
BG = 'Bulgaria',
LUX = 'Luxembourg',
BRA = 'Brazil',
IR = 'Ireland',
IR = 'Ireland',
IE = 'Ireland',
IE = 'Ireland',
Line 706: Line 711:
end
end


local function prettyNamePart(s)
-- Roster "other_class" codes → readable course names (By The Numbers / MITC charts).
return prettyNamePhrase(s, false)
local function expandOtherClassToken(tok)
end
tok = tidyDisplay(tok)
if tok == '' then
return ''
end
local u = mw.ustring.upper(tok)
u = mw.ustring.gsub(u, '%s+', ' ')
u = trim(u)


local function isNegativeClause(s)
local n = mw.ustring.match(u, '^SC%s*B%s*(%d+)$')
local lower = mw.ustring.lower(s)
if n then
if lower == 'failed' or mw.ustring.find(lower, '^failed%s') or mw.ustring.find(lower, '%sfailed%s') or mw.ustring.find(lower, '%sfailed$') then
return 'Special Course B ' .. n
return true
end
n = mw.ustring.match(u, '^SC%s*(%d+)$')
if n then
-- SC# aligns with AGF Photo Interpretation / “Special Course” numbering
-- (e.g. SC8 = Special Course 8 in PI).
return 'Special Course ' .. n
end
n = mw.ustring.match(u, '^DC%s*(%d+)$')
if n then
return 'Division Class ' .. n
end
n = mw.ustring.match(u, '^MITU%s*(%d+)$')
if n then
return 'Mobile Intelligence Training Unit ' .. n
end
n = mw.ustring.match(u, '^RDC%s*(%d+)$')
if n then
return 'Redeployment Class ' .. n
end
n = mw.ustring.match(u, '^RCM%s*(%d+)$')
if n then
return 'Radio Countermeasures Class ' .. n
end
n = mw.ustring.match(u, '^OB%s*(%d+)$')
if n then
return 'Order of Battle Class ' .. n
end
if u == 'SIG' or u == 'SIG-SIG' or mw.ustring.match(u, '^SIG[%s%-]') then
return 'Special Intelligence Group–Signal'
end
end
for _, phrase in ipairs(NEGATIVE_PHRASES) do
 
if mw.ustring.find(lower, phrase, 1, true) then
local unitNum, unitKind = mw.ustring.match(u, '^(%d+)(?:ST|ND|RD|TH)%s+(.+)$')
return true
if unitNum and unitKind then
local kind = trim(unitKind)
if kind == 'INF' then
kind = 'Infantry'
elseif kind == 'ARMD' then
kind = 'Armored'
else
kind = prettyProse(kind)
end
local ord = unitNum
local last = mw.ustring.sub(unitNum, -1)
local last2 = mw.ustring.len(unitNum) >= 2 and mw.ustring.sub(unitNum, -2) or ''
if last2 ~= '11' and last2 ~= '12' and last2 ~= '13' then
if last == '1' then
ord = unitNum .. 'st'
elseif last == '2' then
ord = unitNum .. 'nd'
elseif last == '3' then
ord = unitNum .. 'rd'
else
ord = unitNum .. 'th'
end
else
ord = unitNum .. 'th'
end
end
return ord .. ' ' .. kind
end
end
return false
 
return prettyProse(tok)
end
 
local function expandOtherClass(raw)
raw = tidyDisplay(raw)
if raw == '' then
return ''
end
local parts = {}
for piece in mw.ustring.gmatch(raw, '[^;,]+') do
local expanded = expandOtherClassToken(piece)
if expanded ~= '' then
table.insert(parts, expanded)
end
end
return table.concat(parts, '; ')
end
 
local function prettyNamePart(s)
return prettyNamePhrase(s, false)
end
 
local function isNegativeClause(s)
local lower = mw.ustring.lower(s)
if lower == 'failed' or mw.ustring.find(lower, '^failed%s') or mw.ustring.find(lower, '%sfailed%s') or mw.ustring.find(lower, '%sfailed$') then
return true
end
-- Bare class-chart non-graduate marker (e.g. "NG Class 10 OB", trailing "NG")
if lower == 'ng' or mw.ustring.find(lower, '^ng%s') or mw.ustring.find(lower, '%sng%s') or mw.ustring.find(lower, '%sng$') or mw.ustring.find(lower, 'nongrad') then
return true
end
for _, phrase in ipairs(NEGATIVE_PHRASES) do
if mw.ustring.find(lower, phrase, 1, true) then
return true
end
end
return false
end
end


Line 819: Line 919:
if withPrefix then
if withPrefix then
if dates ~= '' then
if dates ~= '' then
table.insert(out, string.format('[[Class %s]] (%s)', part, dates))
table.insert(out, string.format('[[:Category:Class %s|Class %s]] (%s)', part, part, dates))
else
else
table.insert(out, string.format('[[Class %s]]', part))
table.insert(out, string.format('[[:Category:Class %s|Class %s]]', part, part))
end
end
else
else
if dates ~= '' then
if dates ~= '' then
table.insert(out, string.format('[[Class %s|%s]] (%s)', part, part, dates))
table.insert(out, string.format('[[:Category:Class %s|%s]] (%s)', part, part, dates))
else
else
table.insert(out, string.format('[[Class %s|%s]]', part, part))
table.insert(out, string.format('[[:Category:Class %s|%s]]', part, part))
end
end
end
end
Line 1,458: Line 1,558:
end
end


local function formatAwardsRibbons(raw, rowRaw)
local function awardRibbonRows(raw, rowRaw)
-- Prefer explicit Wikipedia row layout when present
-- Prefer explicit Wikipedia row layout when present
local rows = splitAwardRows(rowRaw or '')
local rows = splitAwardRows(rowRaw or '')
if #rows == 0 then
if #rows > 0 then
raw = trim(raw)
return rows
if raw == '' then
end
return ''
raw = trim(raw)
if raw == '' then
return {}
end
local files = {}
for part in mw.text.gsplit(raw, ';', true) do
part = trim(part)
part = mw.ustring.gsub(part, '^[Ff]ile:', '')
if part ~= '' then
table.insert(files, part)
end
end
local files = {}
end
for part in mw.text.gsplit(raw, ';', true) do
if #files == 0 then
part = trim(part)
return {}
part = mw.ustring.gsub(part, '^[Ff]ile:', '')
end
if part ~= '' then
-- Fallback: pack ~4 across
table.insert(files, part)
local perRow = 4
end
rows = {}
end
local cur = {}
if #files == 0 then
for _, file in ipairs(files) do
return ''
table.insert(cur, file)
end
if #cur == perRow then
-- Fallback: pack ~4 across
local perRow = 4
rows = {}
local cur = {}
for _, file in ipairs(files) do
table.insert(cur, file)
if #cur == perRow then
table.insert(rows, cur)
cur = {}
end
end
if #cur > 0 then
table.insert(rows, cur)
table.insert(rows, cur)
cur = {}
end
end
end
end
-- One table per row so short rows (e.g. 2 ribbons) center like full rows
if #cur > 0 then
local out = {}
table.insert(rows, cur)
for _, cells in ipairs(rows) do
end
table.insert(out, '{| class="soldier-ribbon-rack"')
return rows
end
 
local function awardRowsMatch(ribbonRows, labelRows)
if #ribbonRows == 0 or #ribbonRows ~= #labelRows then
return false
end
for i, cells in ipairs(ribbonRows) do
if #cells ~= #(labelRows[i] or {}) then
return false
end
end
return true
end
 
local function formatAwardsRibbons(raw, rowRaw, labelRaw)
local rows = awardRibbonRows(raw, rowRaw)
if #rows == 0 then
return '', false
end
local labelRows = splitAwardRows(labelRaw or '')
local labeled = awardRowsMatch(rows, labelRows)
-- One table per row so short rows (e.g. 2 ribbons) center like full rows.
-- When labels match ribbon cells, put captions in the same table so each
-- name sits centered under its ribbon (equal column widths).
local out = {}
for i, cells in ipairs(rows) do
if labeled then
table.insert(out, '{| class="soldier-ribbon-rack soldier-awards-labeled"')
else
table.insert(out, '{| class="soldier-ribbon-rack"')
end
table.insert(out, '|-')
table.insert(out, '|-')
for _, file in ipairs(cells) do
for _, file in ipairs(cells) do
file = mw.ustring.gsub(file, '^[Ff]ile:', '')
file = mw.ustring.gsub(file, '^[Ff]ile:', '')
table.insert(out, '| [[File:' .. file .. '|106px|link=]]')
table.insert(out, '| [[File:' .. file .. '|106px|link=]]')
end
if labeled then
table.insert(out, '|-')
for _, label in ipairs(labelRows[i]) do
table.insert(out, '| class="soldier-award-caption" | ' .. label)
end
end
end
table.insert(out, '|}')
table.insert(out, '|}')
end
end
return table.concat(out, '\n')
return table.concat(out, '\n'), labeled
end
end


Line 1,774: Line 1,909:
return ''
return ''
end
end
-- Filled after noteTeamNames (local forward declare; used by p.infobox).
local addTeamsInfoboxRow


function p.infobox(frame)
function p.infobox(frame)
Line 1,814: Line 1,952:
local nativity = expandNativity(parentArg(frame, 'nativity'))
local nativity = expandNativity(parentArg(frame, 'nativity'))
local religion = expandReligion(parentArg(frame, 'religion'))
local religion = expandReligion(parentArg(frame, 'religion'))
local marital = prettyProse(parentArg(frame, 'marital'))
-- education / marital / occupation stay in Technical record (NARA snapshot);
local education = prettyProse(parentArg(frame, 'education'))
-- curated life facts belong in Biography / Timeline, not the infobox.
local occupation = prettyProse(parentArg(frame, 'occupation'))
local knownFor = prettyProse(parentArg(frame, 'known_for'))
local knownFor = prettyProse(parentArg(frame, 'known_for'))
local awards = prettyProse(parentArg(frame, 'awards'))
local awards = prettyProse(parentArg(frame, 'awards'))
Line 1,825: Line 1,962:
awards = trim(first or awards)
awards = trim(first or awards)
end
end
local state = prettyPlace(parentArg(frame, 'state'))


local function datePlaceCell(date, place, year)
local function datePlaceCell(date, place, year)
Line 1,844: Line 1,980:


if alias ~= '' or birth ~= '' or death ~= '' or nativity ~= ''
if alias ~= '' or birth ~= '' or death ~= '' or nativity ~= ''
or religion ~= '' or marital ~= '' or education ~= ''
or religion ~= '' or knownFor ~= '' or awards ~= '' then
or occupation ~= '' or knownFor ~= '' or awards ~= '' or state ~= '' then
addHeader(tbl, 'Personal details')
addHeader(tbl, 'Personal details')
end
end
Line 1,854: Line 1,989:
addRow(tbl, 'Nativity', nativity)
addRow(tbl, 'Nativity', nativity)
addRow(tbl, 'Religion', religion)
addRow(tbl, 'Religion', religion)
addRow(tbl, 'Spouse / marital', marital)
addRow(tbl, 'Education', education)
addRow(tbl, 'Occupation', occupation)
addRow(tbl, 'Known for', knownFor)
addRow(tbl, 'Known for', knownFor)
addRow(tbl, 'Awards', awards)
addRow(tbl, 'Awards', awards)
if state ~= '' then
addRow(tbl, 'State', state)
end


addHeader(tbl, 'Military service')
addHeader(tbl, 'Military service')
Line 1,878: Line 2,007:
addRow(tbl, 'Arm / service', prettyProse(armName))
addRow(tbl, 'Arm / service', prettyProse(armName))
end
end
addRow(tbl, 'Years of service', prettyProse(parentArg(frame, 'years_of_service')))
-- years_of_service / date_arrive / date_depart / certificate stay in Technical
-- record only (rarely curated; certificate is a cryptic chart number).
addRow(tbl, 'ASN', parentArg(frame, 'asn'), 'serial')
addRow(tbl, 'ASN', parentArg(frame, 'asn'), 'serial')


Line 1,899: Line 2,029:
addRow(tbl, 'Camp Ritchie', 'Class ' .. table.concat(classBits, ', '))
addRow(tbl, 'Camp Ritchie', 'Class ' .. table.concat(classBits, ', '))
end
end
addRow(tbl, 'Section', parentArg(frame, 'class_section'))
addRow(tbl, 'Section', mw.ustring.upper(parentArg(frame, 'class_section')))
addRow(tbl, 'Other class', parentArg(frame, 'other_class'))
addRow(tbl, 'Other class', expandOtherClass(parentArg(frame, 'other_class')))


local enlist = prettyPlace(parentArg(frame, 'enlistment_place'))
local enlist = prettyPlace(parentArg(frame, 'enlistment_place'))
Line 1,909: Line 2,039:
addRow(tbl, 'Unit / division', prettyProse(parentArg(frame, 'division')))
addRow(tbl, 'Unit / division', prettyProse(parentArg(frame, 'division')))
addRow(tbl, 'Battles / wars', formatBattles(parentArg(frame, 'battles')))
addRow(tbl, 'Battles / wars', formatBattles(parentArg(frame, 'battles')))
addRow(tbl, 'IPW (German)', teamLinkList(parentArg(frame, 'ipw_ge'), 'IPW ', 'IPW'))
addTeamsInfoboxRow(tbl, frame)
addRow(tbl, 'IPW (Italian)', teamLinkList(parentArg(frame, 'ipw_it'), 'IPW Italian ', 'IPW Italian'))
addRow(tbl, 'Arrived', parentArg(frame, 'date_arrive'))
addRow(tbl, 'Departed', parentArg(frame, 'date_depart'))
addRow(tbl, 'Certificate', realCertificate(parentArg(frame, 'certificate')))


-- Awards ribbons render in the article body (p.awardsSection), not this
-- Awards ribbons render in the article body (p.awardsSection), not this
Line 1,922: Line 2,048:


function p.awardsSection(frame)
function p.awardsSection(frame)
local ribbons = formatAwardsRibbons(
local gridRaw = parentArg(frame, 'awards_grid')
local ribbons, labelsInRack = formatAwardsRibbons(
parentArg(frame, 'awards_ribbons'),
parentArg(frame, 'awards_ribbons'),
parentArg(frame, 'awards_ribbon_rows')
parentArg(frame, 'awards_ribbon_rows'),
gridRaw
)
)
local grid = formatAwardsGrid(parentArg(frame, 'awards_grid'))
-- Skip the separate label table when captions already sit under ribbons
local grid = ''
if not labelsInRack then
grid = formatAwardsGrid(gridRaw)
end
local extras = formatAwardsLegend(parentArg(frame, 'awards_extra'))
local extras = formatAwardsLegend(parentArg(frame, 'awards_extra'))
-- Fallback: flat awards list when no grid was extracted
-- Fallback: flat awards list when no grid was extracted
if grid == '' and extras == '' then
if grid == '' and extras == '' and not labelsInRack then
extras = formatAwardsLegend(parentArg(frame, 'awards'))
extras = formatAwardsLegend(parentArg(frame, 'awards'))
end
end
Line 2,047: Line 2,179:
end
end


function p.noteTeamCategories(frame)
local function appendUniqueTeamName(names, seen, name)
local title = mw.title.getCurrentTitle()
name = trim(tostring(name or ''))
if not title or not title.getContent then
if name == '' or seen[name] then
return ''
return
end
end
local ok, content = pcall(function()
seen[name] = true
return title:getContent()
table.insert(names, name)
end)
end
if not ok or not content then
 
return ''
local function namesFromIpwGerman(raw)
local names = {}
local seen = {}
for part in mw.text.gsplit(raw or '', ';', true) do
part = trim(part)
part = mw.ustring.gsub(part, '[#<>%[%]|{}]', '')
part = trim(part)
if part ~= '' then
local code, shown, mii = normalizeIpwTeamCode(part)
if mii then
appendUniqueTeamName(names, seen, 'MII ' .. mii)
elseif code then
appendUniqueTeamName(names, seen, 'IPW ' .. shown)
end
end
end
end
local bits = {}
return names
for _, name in ipairs(noteTeamNames(researchNotesSection(content))) do
end
table.insert(bits, '[[Category:Team ' .. name .. ']]')
 
local function namesFromIpwItalian(raw)
local names = {}
local seen = {}
for part in mw.text.gsplit(raw or '', ';', true) do
part = trim(part)
part = mw.ustring.gsub(part, '[#<>%[%]|{}]', '')
part = trim(part)
if part ~= '' then
local code = normalizeIpwItalianCode(part)
if code then
appendUniqueTeamName(names, seen, 'IPW Italian ' .. code)
end
end
end
end
return table.concat(bits)
return names
end
end


function p.researchNotes(frame)
--- Explicit |teams= values: "MII 439-G; IPW 12" or shorthand "439-G".
local function namesFromTeamsParam(raw)
local names = {}
local seen = {}
for part in mw.text.gsplit(raw or '', ';', true) do
part = trim(part)
if part ~= '' then
local fromScan = noteTeamNames(part)
if #fromScan > 0 then
for _, name in ipairs(fromScan) do
appendUniqueTeamName(names, seen, name)
end
else
local upper = mw.ustring.upper(part)
local itRest = mw.ustring.match(upper, '^IPW%s+ITALIAN%s+(.+)$')
if itRest then
local code = normalizeIpwItalianCode(itRest)
if code then
appendUniqueTeamName(names, seen, 'IPW Italian ' .. code)
end
else
local ipwRest = mw.ustring.match(upper, '^IPW%s+(.+)$')
if ipwRest then
local code, shown, mii = normalizeIpwTeamCode(ipwRest)
if mii then
appendUniqueTeamName(names, seen, 'MII ' .. mii)
elseif code then
appendUniqueTeamName(names, seen, 'IPW ' .. shown)
end
else
local code, shown, mii = normalizeIpwTeamCode(part)
if mii then
appendUniqueTeamName(names, seen, 'MII ' .. mii)
elseif code and mw.ustring.match(tostring(shown or code), '^%d') then
appendUniqueTeamName(names, seen, 'IPW ' .. shown)
end
end
end
end
end
end
return names
end
 
local function teamNameLink(name)
return string.format('[[Team %s|%s]]', name, name)
end
 
local function pageResearchNotesText()
local title = mw.title.getCurrentTitle()
local title = mw.title.getCurrentTitle()
if not title or not title.getContent then
if not title or not title.getContent then
Line 2,073: Line 2,280:
return title:getContent()
return title:getContent()
end)
end)
if not ok or not content or content == '' then
if not ok or not content then
return ''
return ''
end
end
local section = researchNotesSection(content)
return researchNotesSection(content)
if section == '' then
end
return ''
 
end
--- Roster IPW fields + optional |teams= + research-note team codes, one linked row.
local items = {}
addTeamsInfoboxRow = function(tbl, frame)
for line in mw.text.gsplit(section, '\n', true) do
local names = {}
line = trim(line)
local seen = {}
if mw.ustring.sub(line, 1, 2) == '* ' then
local function addAll(list)
local note = sanitizeNote(mw.ustring.sub(line, 3))
for _, name in ipairs(list) do
if note ~= '' then
appendUniqueTeamName(names, seen, name)
for clause in mw.text.gsplit(note, ';', true) do
clause = trim(clause)
if clause ~= '' and not isNegativeClause(clause) then
table.insert(items, prettyProse(clause))
end
end
end
end
end
end
end
items = joinDateFollowons(items)
addAll(namesFromIpwGerman(parentArg(frame, 'ipw_ge')))
local unique = {}
addAll(namesFromIpwItalian(parentArg(frame, 'ipw_it')))
local seen = {}
addAll(namesFromTeamsParam(parentArg(frame, 'teams')))
for _, clause in ipairs(items) do
addAll(noteTeamNames(pageResearchNotesText()))
local key = normalizeNoteKey(clause)
if #names == 0 then
if key ~= '' and not seen[key] then
return
seen[key] = true
table.insert(unique, clause)
end
end
end
items = unique
local links = {}
if #items == 0 then
for _, name in ipairs(names) do
return ''
table.insert(links, teamNameLink(name))
end
end
local wrap = mw.html.create('div')
local label = 'Team'
wrap:addClass('soldier-research-notes')
if #names > 1 then
wrap:tag('div'):addClass('soldier-research-notes-heading'):wikitext('Research notes')
label = 'Teams'
local ul = wrap:tag('ul')
end
for _, note in ipairs(items) do
addRow(tbl, label, table.concat(links, ', '))
ul:tag('li'):wikitext(note)
end
 
function p.noteTeamCategories(frame)
local title = mw.title.getCurrentTitle()
if not title or not title.getContent then
return ''
end
local ok, content = pcall(function()
return title:getContent()
end)
if not ok or not content then
return ''
end
local seen = {}
local bits = {}
local function addCat(name)
name = trim(tostring(name or ''))
if name == '' or seen[name] then
return
end
seen[name] = true
table.insert(bits, '[[Category:Team ' .. name .. ']]')
end
for _, name in ipairs(noteTeamNames(researchNotesSection(content))) do
addCat(name)
end
for _, name in ipairs(namesFromTeamsParam(parentArg(frame, 'teams'))) do
addCat(name)
end
return table.concat(bits)
end
 
function p.researchNotes(frame)
local title = mw.title.getCurrentTitle()
if not title or not title.getContent then
return ''
end
local ok, content = pcall(function()
return title:getContent()
end)
if not ok or not content or content == '' then
return ''
end
local section = researchNotesSection(content)
if section == '' then
return ''
end
local items = {}
for line in mw.text.gsplit(section, '\n', true) do
line = trim(line)
if mw.ustring.sub(line, 1, 2) == '* ' then
local note = sanitizeNote(mw.ustring.sub(line, 3))
if note ~= '' then
for clause in mw.text.gsplit(note, ';', true) do
clause = trim(clause)
if clause ~= '' and not isNegativeClause(clause) then
table.insert(items, prettyProse(clause))
end
end
end
end
end
items = joinDateFollowons(items)
local unique = {}
local seen = {}
for _, clause in ipairs(items) do
local key = normalizeNoteKey(clause)
if key ~= '' and not seen[key] then
seen[key] = true
table.insert(unique, clause)
end
end
items = unique
if #items == 0 then
return ''
end
local wrap = mw.html.create('div')
wrap:addClass('soldier-research-notes')
wrap:tag('div'):addClass('soldier-research-notes-heading'):wikitext('Research notes')
local ul = wrap:tag('ul')
for _, note in ipairs(items) do
ul:tag('li'):wikitext(note)
end
end
return tostring(wrap)
return tostring(wrap)
end
local function cargoRows(tables, fields, where, limit, orderBy)
if not mw.ext or not mw.ext.cargo or not mw.ext.cargo.query then
return nil
end
local opts = {
where = where,
limit = limit or 8,
}
if orderBy and orderBy ~= '' then
opts.orderBy = orderBy
end
local ok, result = pcall(mw.ext.cargo.query, tables, fields, opts)
if not ok or type(result) ~= 'table' then
return nil
end
return result
end
local function rowField(row, ...)
for i = 1, select('#', ...) do
local key = select(i, ...)
local v = row[key]
if v ~= nil and trim(tostring(v)) ~= '' then
return trim(tostring(v))
end
end
return ''
end
local function titleToCaption(title)
title = trim(title)
local surname, given = mw.ustring.match(title, '^([^,]+),%s*(.-)%s*%(%d')
if surname and given then
return trim(given .. ' ' .. surname)
end
return title
end
local function displayFromCargoRow(row)
local page = rowField(row, 'pageName', '_pageName')
local surname = rowField(row, 'Surname')
local first = rowField(row, 'First_name', 'First name')
local middle = rowField(row, 'Middle_name', 'Middle name')
local given = trim(first .. (middle ~= '' and (' ' .. middle) or ''))
local caption
if given ~= '' and surname ~= '' then
caption = given .. ' ' .. surname
elseif page ~= '' then
caption = titleToCaption(page)
else
caption = surname
end
if page == '' and surname ~= '' then
local asn = rowField(row, 'ASN')
if asn ~= '' then
page = surname .. ', ' .. (given ~= '' and given or first) .. ' (' .. asn .. ')'
end
end
return page, caption
end
local function linkList(rows, skipAsn, skipPage, maxLinks)
local links = {}
local seen = {}
for _, row in ipairs(rows or {}) do
local asn = rowField(row, 'ASN')
local page, caption = displayFromCargoRow(row)
if caption == '' and page ~= '' then
caption = titleToCaption(page)
end
if page ~= '' and page ~= skipPage and asn ~= skipAsn and not seen[page] then
seen[page] = true
table.insert(links, string.format('[[%s|%s]]', page, caption))
if #links >= maxLinks then
break
end
end
end
return links
end
local function classFullWhere(classNum)
-- Prefer Class_number__full: the Class_number HOLDS index is corrupted on some hosts.
local n = tostring(classNum or '')
n = mw.ustring.gsub(n, '[^0-9A-Za-z%-]', '')
if n == '' then
return nil
end
return string.format(
'Class_number__full="%s" OR Class_number__full LIKE "%s;%%" OR Class_number__full LIKE "%%;%s" OR Class_number__full LIKE "%%;%s;%%"',
n,
n,
n,
n
)
end
function p.storyCue(frame)
local story = parentArg(frame, 'story')
if story == '' then
return ''
end
story = mw.ustring.gsub(story, '^%[%[', '')
story = mw.ustring.gsub(story, '%]%]$', '')
local storyTitle, storyLabel = mw.ustring.match(story, '^([^|]+)|(.+)$')
if not storyTitle then
storyTitle = story
storyLabel = nil
end
storyTitle = trim(storyTitle)
if storyLabel then
storyLabel = trim(storyLabel)
end
if not storyLabel or storyLabel == '' then
storyLabel = mw.ustring.match(storyTitle, '([^/]+)$') or storyTitle
end
local name = displayName(frame)
return string.format(
'<div class="soldier-story-cue">'
.. '<span class="soldier-story-cue-label">Full story</span> '
.. 'Read more about %s in [[%s|%s]] on \'\'[[The Stories]]\'\'.'
.. '</div>',
name,
storyTitle,
storyLabel
)
end
-- related-rail: live team rosters via category members (filled by Common.js)
function p.relatedSoldiers(frame)
local pageTitle = mw.title.getCurrentTitle().prefixedText
-- Same sources as the infobox Teams row: roster IPW, |teams=, research notes.
local teamNames = {}
local seenTeams = {}
local function addTeams(list)
for _, name in ipairs(list) do
appendUniqueTeamName(teamNames, seenTeams, name)
end
end
addTeams(namesFromIpwGerman(parentArg(frame, 'ipw_ge')))
addTeams(namesFromIpwItalian(parentArg(frame, 'ipw_it')))
addTeams(namesFromTeamsParam(parentArg(frame, 'teams')))
addTeams(noteTeamNames(pageResearchNotesText()))
if #teamNames == 0 then
return ''
end
local wrap = mw.html.create('div')
:addClass('soldier-related')
:attr('data-current-title', pageTitle)
wrap:tag('div'):addClass('soldier-related-heading'):wikitext('Teammates')
for _, teamName in ipairs(teamNames) do
local cat = 'Category:Team ' .. teamName
local block = wrap:tag('div')
:addClass('soldier-related-block')
:attr('data-team-cat', cat)
block:tag('div')
:addClass('soldier-related-label')
:wikitext('Team — ' .. teamNameLink(teamName))
-- Noscript / pre-JS fallback: category link. Common.js replaces with live names.
block:tag('div')
:addClass('soldier-related-links')
:addClass('soldier-related-links-live')
:wikitext('[[' .. cat .. '|Browse team roster]]')
end
wrap:tag('div')
:addClass('soldier-related-note')
:wikitext(
'These are other soldiers listed on the same team roster. '
.. 'People moved between teams; overlapping service dates are not always documented.'
)
return tostring(wrap)
end
local function seqCount(t)
-- mw.loadData tables do not always report # correctly.
local n = 0
if type(t) ~= 'table' then
return 0
end
for _ in ipairs(t) do
n = n + 1
end
return n
end
function p.mediaCue(frame)
local pageTitle = mw.title.getCurrentTitle().prefixedText
local ok, data = pcall(mw.loadData, 'Module:Interviews/data')
if not ok or type(data) ~= 'table' or type(data.by_title) ~= 'table' then
return ''
end
local rec = data.by_title[pageTitle]
if type(rec) ~= 'table' then
return ''
end
-- Prefer interviews; accept legacy `items` key from hand-edited cues.
local interviews = rec.interviews
if type(interviews) ~= 'table' then
interviews = rec.items
end
local films = rec.films
local nInterviews = seqCount(interviews)
local nFilms = seqCount(films)
if nInterviews == 0 and nFilms == 0 then
return ''
end
local name = displayName(frame)
local blocks = {}
if nInterviews > 0 then
local links = {}
local anyAudio = false
for _, item in ipairs(interviews) do
local heading = item.heading or 'Interview'
local short = item.short or heading
if item.kind == 'audio' then
anyAudio = true
end
table.insert(
links,
string.format('[[Ritchie Boy Interviews#%s|%s]]', heading, short)
)
end
local label = nInterviews == 1 and 'Interview' or 'Interviews'
local verb = 'Watch'
if nInterviews == 1 and interviews[1].kind == 'audio' then
verb = 'Listen to'
elseif anyAudio and nInterviews > 1 then
verb = 'Hear'
end
local body
if nInterviews == 1 then
body = string.format(
'%s %s on [[Ritchie Boy Interviews#%s|%s]].',
verb,
name,
interviews[1].heading,
interviews[1].short or interviews[1].heading
)
else
body = string.format(
'%s %s in %d recordings on [[Ritchie Boy Interviews]]: %s.',
verb,
name,
nInterviews,
table.concat(links, ' · ')
)
end
table.insert(
blocks,
'<div class="soldier-interview-cue">'
.. '<span class="soldier-interview-cue-label">'
.. label
.. '</span> '
.. body
.. '</div>'
)
end
if nFilms > 0 then
local links = {}
for _, item in ipairs(films) do
local heading = item.heading or 'Documentaries/Movies'
local filmLabel = item.label or heading
table.insert(
links,
string.format('[[Documentaries/Movies#%s|%s]]', heading, filmLabel)
)
end
local body = string.format(
'%s appears in %s.',
name,
table.concat(links, '; ')
)
table.insert(
blocks,
'<div class="soldier-film-cue">'
.. '<span class="soldier-film-cue-label">On film</span> '
.. body
.. '</div>'
)
end
return '<div class="soldier-media-cue">' .. table.concat(blocks, '') .. '</div>'
end
end


return p
return p