Module:Soldier: Difference between revisions
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 | -- Roster "other_class" codes → readable course names (By The Numbers / MITC charts). | ||
return | local function expandOtherClassToken(tok) | ||
tok = tidyDisplay(tok) | |||
if tok == '' then | |||
return '' | |||
end | |||
local u = mw.ustring.upper(tok) | |||
u = mw.ustring.gsub(u, '%s+', ' ') | |||
u = trim(u) | |||
local | local n = mw.ustring.match(u, '^SC%s*B%s*(%d+)$') | ||
if n then | |||
if | return 'Special Course B ' .. n | ||
return | 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 | ||
if mw.ustring. | local unitNum, unitKind = mw.ustring.match(u, '^(%d+)(?:ST|ND|RD|TH)%s+(.+)$') | ||
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 | 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 | if #rows > 0 then | ||
raw = trim(raw) | return rows | ||
end | |||
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 | ||
end | |||
if #files == 0 then | |||
return {} | |||
end | |||
-- 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) | 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 | 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')) | ||
-- education / marital / occupation stay in Technical record (NARA snapshot); | |||
-- curated life facts belong in Biography / Timeline, not the infobox. | |||
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 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 religion ~= '' or knownFor ~= '' or awards ~= '' 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, 'Known for', knownFor) | addRow(tbl, 'Known for', knownFor) | ||
addRow(tbl, 'Awards', awards) | addRow(tbl, 'Awards', awards) | ||
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 | ||
-- 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'))) | ||
addTeamsInfoboxRow(tbl, frame) | |||
-- 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( | -- 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 | local function appendUniqueTeamName(names, seen, name) | ||
name = trim(tostring(name or '')) | |||
if | if name == '' or seen[name] then | ||
return | return | ||
end | end | ||
local | seen[name] = true | ||
table.insert(names, name) | |||
end | |||
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 | return names | ||
for | end | ||
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 | return names | ||
end | end | ||
function | --- 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 | if not ok or not content then | ||
return '' | return '' | ||
end | end | ||
return researchNotesSection(content) | |||
end | |||
--- Roster IPW fields + optional |teams= + research-note team codes, one linked row. | |||
local | addTeamsInfoboxRow = function(tbl, frame) | ||
local names = {} | |||
local seen = {} | |||
local function addAll(list) | |||
for _, name in ipairs(list) do | |||
appendUniqueTeamName(names, seen, name) | |||
end | end | ||
end | end | ||
addAll(namesFromIpwGerman(parentArg(frame, 'ipw_ge'))) | |||
addAll(namesFromIpwItalian(parentArg(frame, 'ipw_it'))) | |||
addAll(namesFromTeamsParam(parentArg(frame, 'teams'))) | |||
addAll(noteTeamNames(pageResearchNotesText())) | |||
if #names == 0 then | |||
return | |||
end | end | ||
local links = {} | |||
for _, name in ipairs(names) do | |||
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 | ||