Fix mediaCue count on loadData tables
Infobox: drop certificate/arrive/depart/years; expand other_class codes
 
(9 intermediate revisions by the same user not shown)
Line 711: 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 824: 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,463: 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
table.insert(out, '|-')
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, '|-')
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,822: 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,833: 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,852: 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,862: 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,886: 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,907: 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,918: Line 2,040:
addRow(tbl, 'Battles / wars', formatBattles(parentArg(frame, 'battles')))
addRow(tbl, 'Battles / wars', formatBattles(parentArg(frame, 'battles')))
addTeamsInfoboxRow(tbl, frame)
addTeamsInfoboxRow(tbl, frame)
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,929: 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,400: Line 2,525:
end
end


-- related-rail: teams-only cache bump
-- related-rail: live team rosters via category members (filled by Common.js)
function p.relatedSoldiers(frame)
function p.relatedSoldiers(frame)
local pageTitle = mw.title.getCurrentTitle().prefixedText
local pageTitle = mw.title.getCurrentTitle().prefixedText
local sections = {}
local rosterData = nil
local okData, loaded = pcall(mw.loadData, 'Module:TeamRosters/data')
if okData and type(loaded) == 'table' then
rosterData = loaded
end
local function teamMemberLinks(teamName, maxLinks)
maxLinks = maxLinks or 24
local members = rosterData and rosterData[teamName] or nil
if type(members) ~= 'table' then
return {}
end
local links = {}
for _, title in ipairs(members) do
title = trim(tostring(title or ''))
if title ~= '' and title ~= pageTitle then
table.insert(links, string.format('[[%s|%s]]', title, titleToCaption(title)))
if #links >= maxLinks then
break
end
end
end
return links
end
local function addTeamBlock(teamName)
local head = teamNameLink(teamName)
local cat = 'Category:Team ' .. teamName
local links = teamMemberLinks(teamName, 24)
if #links > 0 then
table.insert(
sections,
'<div class="soldier-related-block"><div class="soldier-related-label">Team — '
.. head
.. '</div><div class="soldier-related-links">'
.. table.concat(links, ' · ')
.. ' · [['
.. cat
.. '|full roster]]</div></div>'
)
else
table.insert(
sections,
'<div class="soldier-related-block"><div class="soldier-related-label">Team — '
.. head
.. '</div><div class="soldier-related-links">[['
.. cat
.. '|Browse soldiers listed on this team]]</div></div>'
)
end
end


-- Same sources as the infobox Teams row: roster IPW, |teams=, research notes.
-- Same sources as the infobox Teams row: roster IPW, |teams=, research notes.
Line 2,469: Line 2,541:
addTeams(namesFromTeamsParam(parentArg(frame, 'teams')))
addTeams(namesFromTeamsParam(parentArg(frame, 'teams')))
addTeams(noteTeamNames(pageResearchNotesText()))
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
for _, teamName in ipairs(teamNames) do
addTeamBlock(teamName)
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
end


if #sections == 0 then
wrap:tag('div')
return ''
:addClass('soldier-related-note')
end
:wikitext(
return '<div class="soldier-related">'
'These are other soldiers listed on the same team roster. '
.. '<div class="soldier-related-heading">Teammates</div>'
.. 'People moved between teams; overlapping service dates are not always documented.'
.. table.concat(sections, '\n')
)
.. '<div class="soldier-related-note">These are other soldiers listed on the same team roster. '
return tostring(wrap)
.. 'People moved between teams; overlapping service dates are not always documented.</div>'
.. '</div>'
end
end


Line 2,507: Line 2,597:
return ''
return ''
end
end
-- Prefer interviews; accept legacy `items` key from hand-edited cues.
local interviews = rec.interviews
local interviews = rec.interviews
if type(interviews) ~= 'table' then
interviews = rec.items
end
local films = rec.films
local films = rec.films
local nInterviews = seqCount(interviews)
local nInterviews = seqCount(interviews)
Line 2,518: Line 2,612:
if nInterviews > 0 then
if nInterviews > 0 then
local links = {}
local links = {}
local anyAudio = false
for _, item in ipairs(interviews) do
for _, item in ipairs(interviews) do
local heading = item.heading or 'Interview'
local heading = item.heading or 'Interview'
local short = item.short or heading
local short = item.short or heading
if item.kind == 'audio' then
anyAudio = true
end
table.insert(
table.insert(
links,
links,
Line 2,527: Line 2,625:
end
end
local label = nInterviews == 1 and 'Interview' or 'Interviews'
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
local body
if nInterviews == 1 then
if nInterviews == 1 then
body = string.format(
body = string.format(
'Watch %s on [[Ritchie Boy Interviews#%s|%s]].',
'%s %s on [[Ritchie Boy Interviews#%s|%s]].',
verb,
name,
name,
interviews[1].heading,
interviews[1].heading,
Line 2,537: Line 2,642:
else
else
body = string.format(
body = string.format(
'Watch %s in %d videos on [[Ritchie Boy Interviews]]: %s.',
'%s %s in %d recordings on [[Ritchie Boy Interviews]]: %s.',
verb,
name,
name,
nInterviews,
nInterviews,